README
¶
dsops Test Infrastructure
Last Updated: 2025-11-15 Purpose: Test utilities, fixtures, and Docker-based integration test infrastructure For: Contributors writing tests for dsops
Overview
This directory contains shared test infrastructure used by unit tests, integration tests, and end-to-end tests throughout the dsops codebase. The test utilities provide consistent patterns for building test configurations, managing Docker environments, capturing logs, and validating behavior.
Directory Structure
tests/
├── README.md # This file
├── integration/ # Integration tests with Docker
│ ├── docker-compose.yml # Test service definitions
│ ├── providers/ # Provider integration tests
│ ├── rotation/ # Rotation workflow tests
│ └── e2e/ # End-to-end CLI tests
├── fixtures/ # Test data and configurations
│ ├── configs/ # Test dsops.yaml files
│ ├── secrets/ # Mock secret data (JSON)
│ └── services/ # Service definitions
├── fakes/ # Manual test doubles
│ └── provider_fake.go # Fake provider.Provider implementation
├── mocks/ # Generated mocks (mockgen)
│ └── (generated files)
└── testutil/ # Test utilities and helpers
├── assert.go # Custom assertions (AssertSecretRedacted, etc.)
├── config.go # Config builders (TestConfigBuilder)
├── contract.go # Provider contract tests
├── docker.go # Docker environment management
├── env.go # Environment variable helpers
├── fixtures.go # Fixture loading
└── logger.go # Log capture (TestLogger)
Test Utilities
testutil Package
Import test utilities:
import "github.com/systmms/dsops/tests/testutil"
TestConfigBuilder
Programmatically build test configurations without manual YAML:
func TestMyFeature(t *testing.T) {
// Build config programmatically
builder := testutil.NewTestConfig(t).
WithSecretStore("vault", "vault", map[string]any{
"addr": "http://localhost:8200",
"token": "test-token",
}).
WithEnv("test", map[string]config.Variable{
"DATABASE_URL": {
From: "store://vault/database/url",
},
})
defer builder.Cleanup() // Removes temp files
// Get in-memory config
cfg := builder.Build()
// Or write to temp file
configPath := builder.Write()
// Use in tests
assert.NotNil(t, cfg.SecretStores["vault"])
}
Methods:
NewTestConfig(t)- Create new builderWithSecretStore(name, type, config)- Add secret storeWithService(name, type, config)- Add serviceWithEnv(name, variables)- Add environmentWithProvider(name, type, config)- Add legacy providerBuild()- Get in-memory configWrite()- Write to temp file, return pathCleanup()- Remove temp files (automatic via t.Cleanup)
FakeProvider
Manual fake implementation of provider.Provider interface:
import "github.com/systmms/dsops/tests/fakes"
func TestResolution(t *testing.T) {
// Create fake provider with test data
fake := fakes.NewFakeProvider("test").
WithSecret("db/password", provider.SecretValue{
Value: map[string]string{"password": "test-123"},
}).
WithSecret("api/key", provider.SecretValue{
Value: map[string]string{"key": "test-key-456"},
}).
WithError("bad/secret", errors.New("not found"))
// Use fake in tests
resolver := resolve.NewResolver(fake)
secret, err := resolver.Resolve(ctx, "store://test/db/password")
assert.NoError(t, err)
assert.Equal(t, "test-123", secret.Value["password"])
// Verify call count
assert.Equal(t, 1, fake.GetCallCount("Resolve"))
}
Methods:
NewFakeProvider(name)- Create fakeWithSecret(key, value)- Add secret dataWithMetadata(key, metadata)- Add metadataWithError(key, err)- Make key return errorWithDelay(duration)- Simulate network latencyWithCapability(cap, supported)- Set capability flagGetCallCount(method)- Get call count for inspectionResetCallCount()- Reset counters
DockerTestEnv
Manage Docker Compose services for integration tests:
func TestVaultIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
// Start Docker services
env := testutil.StartDockerEnv(t, []string{"vault", "postgres"})
defer env.Stop() // Automatic cleanup
// Wait for health checks
require.NoError(t, env.WaitForHealthy(30*time.Second))
// Get service clients
vaultClient := env.VaultClient()
pgClient := env.PostgresClient()
// Seed test data
err := vaultClient.Write("secret/test", map[string]any{
"password": "test-secret-123",
})
require.NoError(t, err)
// Get provider config
vaultConfig := env.VaultConfig()
// Create and test provider
provider := providers.NewVaultProvider(vaultConfig)
secret, err := provider.Resolve(ctx, ref)
assert.NoError(t, err)
}
Methods:
StartDockerEnv(t, services)- Start Docker Compose with specified servicesSkipIfDockerUnavailable(t)- Skip test if Docker not availableIsDockerAvailable()- Check if Docker is installedWaitForHealthy(timeout)- Wait for service health checksVaultClient()- Get Vault test clientPostgresClient()- Get PostgreSQL connectionLocalStackClient()- Get LocalStack AWS clientMongoClient()- Get MongoDB clientVaultConfig()- Get provider config mapPostgresConfig()- Get provider config mapStop()- Stop Docker services
TestLogger
Capture log output for redaction validation:
func TestSecretRedaction(t *testing.T) {
logger := testutil.NewTestLogger(t)
// Log a secret
secretValue := "super-secret-password"
logger.Logger().Info("Retrieved secret: %s", logging.Secret(secretValue))
// Validate redaction
output := logger.GetOutput()
logger.AssertContains(t, "[REDACTED]")
logger.AssertNotContains(t, secretValue)
logger.AssertRedacted(t, secretValue)
}
Methods:
NewTestLogger(t)- Create logger with default levelNewTestLoggerWithLevel(t, level)- Create with specific levelCapture(fn)- Capture logs from functionGetOutput()- Get captured log outputClear()- Clear captured logsAssertContains(t, substr)- Assert substring presentAssertNotContains(t, substr)- Assert substring absentAssertRedacted(t, secretValue)- Assert secret redactedLogger()- Get underlying logger instance
Provider Contract Tests
Validate provider implements interface correctly:
func TestMyProviderContract(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
// Setup test environment (Docker, etc.)
env := testutil.StartDockerEnv(t, []string{"myprovider"})
defer env.Stop()
// Seed test data
testData := map[string]provider.SecretValue{
"test/secret1": {
Value: map[string]string{"password": "test-123"},
},
"test/secret2": {
Value: map[string]string{"api_key": "key-456"},
},
}
for key, secret := range testData {
require.NoError(t, env.MyProviderClient().CreateSecret(key, secret.Value))
}
// Create provider
provider := providers.NewMyProvider(env.MyProviderConfig())
// Run ALL contract tests
tc := testutil.ProviderTestCase{
Name: "myprovider",
Provider: provider,
TestData: testData,
}
testutil.RunProviderContractTests(t, tc)
}
Contract tests verify:
Name()returns non-empty stringResolve()retrieves correct secret valuesDescribe()returns metadata (not secret values)Capabilities()returns valid capability flagsValidate()checks provider configuration- Error handling for missing secrets
- Concurrent access is thread-safe
Fixtures
Test Configurations
Pre-built configuration files for common test scenarios:
func TestConfigLoading(t *testing.T) {
fixtures := testutil.NewTestFixture(t)
// Load pre-defined config
cfg := fixtures.LoadConfig("simple.yaml")
assert.NotNil(t, cfg)
}
Available fixtures:
simple.yaml- Basic single-provider configmulti-provider.yaml- Multiple secret storesrotation.yaml- Rotation-enabled config
Custom Fixtures
Add new fixtures to tests/fixtures/configs/:
# tests/fixtures/configs/my-test.yaml
version: 1
secretStores:
test:
type: literal
values:
DATABASE_URL: "postgres://localhost/testdb"
API_KEY: "test-api-key-123"
envs:
test:
DATABASE_URL:
from: store://test/DATABASE_URL
API_KEY:
from: store://test/API_KEY
Load in tests:
cfg := fixtures.LoadConfig("my-test.yaml")
Docker Integration
Docker Compose Services
Integration tests use Docker Compose to run real service implementations:
Available services:
vault- HashiCorp Vault (secret storage)postgres- PostgreSQL (database rotation testing)localstack- LocalStack (AWS service emulation)mongodb- MongoDB (database rotation testing)
Starting Services
Start specific services:
env := testutil.StartDockerEnv(t, []string{"vault"})
defer env.Stop()
Start multiple services:
env := testutil.StartDockerEnv(t, []string{"vault", "postgres", "localstack"})
defer env.Stop()
Service Configuration
Services are defined in tests/integration/docker-compose.yml with:
- Health checks (automatic readiness detection)
- Port mappings (localhost access)
- Environment variables (test credentials)
- Volumes (data persistence across tests)
Example:
vault:
image: hashicorp/vault:1.15
ports:
- "8200:8200"
environment:
VAULT_DEV_ROOT_TOKEN_ID: test-root-token
healthcheck:
test: ["CMD", "vault", "status"]
interval: 2s
retries: 10
Running Integration Tests
Local development:
# Run with Docker
make test-integration
# Skip integration tests (fast)
go test -short ./...
CI/CD:
- Integration tests automatically run in GitHub Actions
- Docker services started via docker-compose
- Tests skip if Docker unavailable
PostgreSQL Test Limitations
The PostgreSQL integration tests use the lib/pq driver, which has known limitations with concurrent DDL operations on system catalogs. Specifically:
- ❌ Avoid: Concurrent CREATE/DROP/ALTER USER operations
- ✅ OK: Concurrent SELECT/INSERT/UPDATE queries
- ✅ OK: Sequential user management operations
If you encounter "pq: invalid message format" errors in tests involving concurrent user creation, this is a driver limitation. Use sequential tests or the connection_pool_compatibility test as a reference for concurrent query patterns.
Best Practices
Use Fakes for Unit Tests
✅ DO: Use FakeProvider for unit tests
fake := fakes.NewFakeProvider("test").WithSecret(...)
resolver := resolve.NewResolver(fake)
❌ DON'T: Use Docker for pure logic tests
// Slow and unnecessary
env := testutil.StartDockerEnv(t, []string{"vault"})
Skip Integration Tests in Short Mode
✅ DO: Check testing.Short()
func TestIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
env := testutil.StartDockerEnv(t, []string{"vault"})
// ...
}
Clean Up Resources
✅ DO: Use defer for cleanup
env := testutil.StartDockerEnv(t, []string{"vault"})
defer env.Stop() // Guarantees cleanup even if test fails
builder := testutil.NewTestConfig(t)
defer builder.Cleanup() // Automatic via t.Cleanup()
Don't Leak Secrets in Fixtures
✅ DO: Use fake/mock secrets
# tests/fixtures/secrets/test-secrets.json
{
"password": "test-fake-password-123",
"api_key": "test-fake-key-456"
}
❌ DON'T: Use real credentials
# BAD - never commit real secrets
{
"password": "MyActualPassword123!",
"api_key": "sk-real-openai-key"
}
Parallel Unit Tests
✅ DO: Use t.Parallel() for independent tests
func TestPureLogic(t *testing.T) {
t.Parallel() // Safe - no shared state
result := Process("input")
assert.Equal(t, "output", result)
}
❌ DON'T: Parallelize integration tests
func TestVault(t *testing.T) {
// NO t.Parallel() - Docker ports conflict
env := testutil.StartDockerEnv(t, []string{"vault"})
// ...
}
Troubleshooting
Docker Services Not Starting
Symptom: Integration tests fail with connection errors
Solution:
# Check Docker is running
docker ps
# Manually start services
cd tests/integration
docker-compose up -d
# Check service health
docker-compose ps
docker-compose logs vault
# Stop services
docker-compose down
Port Conflicts
Symptom: bind: address already in use
Solution:
# Find process using port
lsof -i :8200
# Stop conflicting service
docker-compose down
# Or kill process
kill -9 <PID>
Tests Fail Only in CI
Symptom: Tests pass locally but fail in GitHub Actions
Common causes:
- Race condition (run
go test -race ./...locally) - Docker not available (check
testing.Short()) - Timing differences (use health checks, not
time.Sleep()) - File path assumptions (use
t.TempDir())
Performance Tips
Optimize test execution:
- Use
-shortflag during development - Share Docker containers between tests
- Run unit tests in parallel (
t.Parallel()) - Cache Docker images locally
- Use test fixtures instead of rebuilding configs
Fast iteration:
# Unit tests only (fast)
go test -short -v ./internal/resolve
# Watch mode (requires entr)
find . -name '*.go' | entr -c go test -short ./...
Further Reading
- TDD Workflow Guide - Red-Green-Refactor cycle
- Testing Strategy - Overview of test categories
- Test Patterns - Common patterns
- Quick Start - Quick reference
Questions? See SPEC-005 or ask in GitHub Discussions.