Testing Guide#
Comprehensive testing is essential for maintaining code quality. This guide covers our testing practices and how to write effective tests.
Test Structure#
tests/
├── integration/ # Integration tests
│ ├── fixtures/ # Test fixtures (manifest, catalog, expected output)
│ ├── cli_test.go # CLI integration tests
│ └── integration_test.go # Full workflow tests
└── ...
pkg/
├── parsers/
│ ├── parser.go
│ └── parser_test.go # Unit tests alongside code
├── generators/
│ ├── generator.go
│ └── generator_test.go
└── ...
🎯 Types of Tests#
Unit Tests#
Test individual functions and methods in isolation.
Location: pkg/*/
Example:
func TestDimensionGenerator_GenerateDimension(t *testing.T) {
tests := []struct {
name string
column models.DbtModelColumn
expected models.LookMLDimension
}{
{
name: "string dimension",
column: models.DbtModelColumn{
Name: "customer_name",
Type: "STRING",
},
expected: models.LookMLDimension{
Name: "customer_name",
Type: "string",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gen := NewDimensionGenerator(&config.Config{})
result := gen.GenerateDimension(tt.column)
assert.Equal(t, tt.expected.Name, result.Name)
assert.Equal(t, tt.expected.Type, result.Type)
})
}
}
Integration Tests#
Test complete workflows with real fixtures.
Location: tests/integration/
Running:
# All integration tests
go test ./tests/integration/...
# Specific test
go test ./tests/integration/ -run TestFixtureComparison
# With verbose output
go test -v ./tests/integration/...
Table-Driven Tests#
Our preferred pattern for comprehensive test coverage.
func TestParser(t *testing.T) {
tests := []struct {
name string
input string
expected string
expectError bool
}{
{"valid input", "test", "TEST", false},
{"empty input", "", "", true},
{"special chars", "a@b", "A@B", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Parse(tt.input)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
🎬 Running Tests#
Basic Commands#
# All tests
make test
# With race detector
make test-race
# Short mode (skip long tests)
go test -short ./...
# Verbose output
go test -v ./...
# Specific package
go test ./pkg/generators/...
# Specific test
go test ./pkg/parsers -run TestModelParser
Coverage#
# Generate coverage report
make test-coverage
# View in browser
open coverage.html
# Coverage for specific package
go test -cover ./pkg/generators/
Coverage Goals#
- Overall: > 80%
- pkg/parsers: > 85%
- pkg/generators: > 85%
- pkg/models: > 75%
Check current coverage:
make test-coverage
Writing Good Tests#
DO ✅#
// Clear test names
func TestParseManifest_WithValidJSON_ReturnsModels(t *testing.T) { }
// Test error cases
func TestGenerator_WithInvalidModel_ReturnsError(t *testing.T) { }
// Use table-driven tests
tests := []struct {
name string
// ...
}{}
// Test edge cases
{"empty input", "", "", true},
{"nil input", nil, nil, true},
{"max length", strings.Repeat("a", 1000), ...},
DON’T ❌#
// Vague test names
func TestParse(t *testing.T) { }
// No error checking
result := Parse(input)
// Missing: if err != nil
// Hard-coded values without explanation
assert.Equal(t, 42, result) // Why 42?
// No cleanup
file := createTempFile()
// Missing: defer os.Remove(file)
Testing Utilities#
Assertions#
We use testify:
import "github.com/stretchr/testify/assert"
assert.Equal(t, expected, actual)
assert.NoError(t, err)
assert.Error(t, err)
assert.Contains(t, haystack, needle)
assert.Len(t, slice, 3)
Test Fixtures#
Located in tests/integration/fixtures/
:
// Load fixture
manifest := loadFixture(t, "fixtures/manifest.json")
// Helper function
func loadFixture(t *testing.T, path string) map[string]interface{} {
data, err := os.ReadFile(path)
require.NoError(t, err)
var result map[string]interface{}
require.NoError(t, json.Unmarshal(data, &result))
return result
}
🐛 Debugging Tests#
Verbose Output#
go test -v ./pkg/parsers/...
Specific Test#
go test -v ./pkg/parsers -run TestModelParser_FilterModels
Print Debug Info#
func TestDebug(t *testing.T) {
result := Parse(input)
t.Logf("Result: %+v", result) // Only shows on failure or -v
fmt.Printf("Debug: %+v\n", result) // Always shows
}
Race Detector#
go test -race ./...
Performance Testing#
Benchmarks#
func BenchmarkParser(b *testing.B) {
parser := NewParser()
b.ResetTimer()
for i := 0; i < b.N; i++ {
parser.Parse(testData)
}
}
Run benchmarks:
go test -bench=. ./pkg/parsers/
go test -bench=BenchmarkParser -benchmem
CI/CD Testing#
Our CI runs:
# Format check
gofmt -l .
# Linting
golangci-lint run
# Vet
go vet ./...
# Tests with race detector
go test -race -v ./...
# Coverage
go test -coverprofile=coverage.out ./...
Run the same locally:
make ci-check
📝 Best Practices#
- Test behavior, not implementation
- Keep tests simple and readable
- One assertion per test case (when possible)
- Clean up resources (use defer)
- Avoid test interdependencies
- Mock external dependencies
- Test error paths
- Document complex test scenarios