Testing
This guide covers testing strategies for code that uses fig, including mocking secret providers and managing environment variables in tests.
Test Utilities
fig provides test helpers in the testing package:
import figtesting "github.com/zoobzio/fig/testing"
MockProvider
A simple in-memory secret provider for tests:
func TestConfigWithSecrets(t *testing.T) {
provider := figtesting.NewMockProvider(t, map[string]string{
"db/password": "test-password",
"api/key": "test-api-key",
})
var cfg Config
if err := fig.Load(&cfg, provider); err != nil {
t.Fatal(err)
}
if cfg.Password != "test-password" {
t.Errorf("Password = %q, want %q", cfg.Password, "test-password")
}
}
MockProvider returns ErrSecretNotFound for missing keys, matching the expected provider behaviour.
SetEnv
Sets an environment variable for the test's duration:
func TestConfigFromEnv(t *testing.T) {
figtesting.SetEnv(t, "APP_HOST", "test.example.com")
figtesting.SetEnv(t, "APP_PORT", "9000")
var cfg Config
if err := fig.Load(&cfg); err != nil {
t.Fatal(err)
}
if cfg.Host != "test.example.com" {
t.Errorf("Host = %q, want %q", cfg.Host, "test.example.com")
}
}
The helper uses t.Setenv internally, so variables are automatically restored after the test.
Testing Patterns
Testing Default Values
Verify defaults by loading without environment variables:
func TestDefaults(t *testing.T) {
var cfg Config
if err := fig.Load(&cfg); err != nil {
t.Fatal(err)
}
if cfg.Port != 8080 {
t.Errorf("Port = %d, want default 8080", cfg.Port)
}
}
Testing Required Fields
Verify that missing required fields fail:
func TestRequiredFields(t *testing.T) {
var cfg struct {
APIKey string `env:"TEST_API_KEY" required:"true"`
}
err := fig.Load(&cfg)
if err == nil {
t.Fatal("expected error for missing required field")
}
var fieldErr *fig.FieldError
if !errors.As(err, &fieldErr) {
t.Fatalf("expected FieldError, got %T", err)
}
if fieldErr.Field != "APIKey" {
t.Errorf("FieldError.Field = %q, want %q", fieldErr.Field, "APIKey")
}
}
Testing Validation
Test the Validator interface:
type validatedConfig struct {
Port int `env:"TEST_PORT" default:"8080"`
}
func (c *validatedConfig) Validate() error {
if c.Port <= 0 || c.Port > 65535 {
return errors.New("port must be between 1 and 65535")
}
return nil
}
func TestValidation(t *testing.T) {
figtesting.SetEnv(t, "TEST_PORT", "-1")
var cfg validatedConfig
err := fig.Load(&cfg)
if err == nil {
t.Fatal("expected validation error")
}
if err.Error() != "port must be between 1 and 65535" {
t.Errorf("unexpected error: %v", err)
}
}
Testing Secret Provider Errors
Test that provider errors propagate correctly:
type failingProvider struct {
err error
}
func (p *failingProvider) Get(ctx context.Context, key string) (string, error) {
return "", p.err
}
func TestProviderError(t *testing.T) {
provider := &failingProvider{err: errors.New("network timeout")}
var cfg struct {
Secret string `secret:"db/password"`
}
err := fig.Load(&cfg, provider)
if err == nil {
t.Fatal("expected error from provider")
}
var fieldErr *fig.FieldError
if !errors.As(err, &fieldErr) {
t.Fatalf("expected FieldError, got %T", err)
}
}
Testing Resolution Order
Verify that secrets take precedence over environment variables:
func TestSecretPrecedence(t *testing.T) {
figtesting.SetEnv(t, "DB_PASSWORD", "env-password")
provider := figtesting.NewMockProvider(t, map[string]string{
"db/password": "secret-password",
})
var cfg struct {
Password string `secret:"db/password" env:"DB_PASSWORD"`
}
if err := fig.Load(&cfg, provider); err != nil {
t.Fatal(err)
}
if cfg.Password != "secret-password" {
t.Errorf("Password = %q, want secret to take precedence", cfg.Password)
}
}
Integration Testing
For integration tests against real secret providers, use build tags:
//go:build integration
package myapp_test
import (
"context"
"testing"
"github.com/zoobzio/fig"
"github.com/zoobzio/fig/vault"
)
func TestVaultIntegration(t *testing.T) {
provider, err := vault.New()
if err != nil {
t.Skip("Vault not available")
}
var cfg Config
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := fig.LoadContext(ctx, &cfg, provider); err != nil {
t.Fatal(err)
}
// Assert expected values from test Vault instance
}
Run with:
go test -tags=integration ./...
Table-Driven Tests
For comprehensive configuration testing:
func TestConfigScenarios(t *testing.T) {
tests := []struct {
name string
env map[string]string
secrets map[string]string
wantHost string
wantPort int
wantErr bool
}{
{
name: "defaults",
wantHost: "localhost",
wantPort: 8080,
},
{
name: "env override",
env: map[string]string{"APP_HOST": "prod.example.com"},
wantHost: "prod.example.com",
wantPort: 8080,
},
{
name: "missing required",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.env {
figtesting.SetEnv(t, k, v)
}
provider := figtesting.NewMockProvider(t, tt.secrets)
var cfg Config
err := fig.Load(&cfg, provider)
if tt.wantErr {
if err == nil {
t.Error("expected error")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Host != tt.wantHost {
t.Errorf("Host = %q, want %q", cfg.Host, tt.wantHost)
}
if cfg.Port != tt.wantPort {
t.Errorf("Port = %d, want %d", cfg.Port, tt.wantPort)
}
})
}
}
Next Steps
- Troubleshooting — common errors and debugging
- Reference — full API documentation