zoobzio January 23, 2026 Edit this page

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