Testing Patterns
Analysis Date: 2026-02-05
Test Framework#
Runner:
- Go's built-in
testingpackage (standard library) - Test execution:
go test ./...from project root - Test discovery:
*_test.gofiles in same directory as source
Assertion Library:
- No external assertion library (e.g., testify/assert) used
- Manual assertions with if/else comparisons
- Test reporting:
t.Errorf(),t.Error(),t.Fatal(),t.Fatalf()
Run Commands:
make test # Run all tests with coverage analysis (85% threshold)
make test-fast # Run tests without coverage (faster, uses cache)
./scripts/run_tests.sh # Run with advanced coverage reporting
./scripts/run_tests.sh --verbose # Verbose test output
./scripts/run_tests.sh --fast # Fast mode without coverage
UPDATE_GOLDEN=1 go test -run TestHTTPGenGoldenFiles # Update golden files
go test -v -run TestLowerFirst # Run specific unit testsCoverage:
- Required threshold: 85% for packages
- Coverage reports generated in
coverage/directory:coverage.out: Coverage profilecoverage.html: Interactive HTML coverage reportcoverage.json: Machine-readable JSON reportcoverage-badge.svg: Coverage badge for README
- Coverage mode: Runs with
-raceflag for race condition detection - Fast mode: Skips coverage analysis entirely for faster feedback loops
Test File Organization#
Location:
- Co-located with source files in same directory
- Test files suffix:
_test.go - Golden file tests:
*_golden_test.go(regression tests)
Naming:
- Test functions:
Test<FunctionName>(e.g.,TestLowerFirst(),TestCamelToSnake()) - Golden file tests:
TestHTTPGenGoldenFiles,TestExhaustiveGoldenFiles,TestTSClientGoldenFiles - Helper functions in tests: Any function prefixed with lowercase (e.g.,
generateTestFiles(),reportFirstDifference())
Structure:
internal/httpgen/
├── generator.go
├── generator_test.go # Unit tests
├── golden_test.go # Golden file regression tests
├── validation_test.go # Validation-specific tests
├── annotations_test.go # Annotation parsing tests
├── error_handler_test.go # Error handling tests
├── unwrap_test.go # Unwrap feature tests
└── testdata/
├── proto/
│ ├── http_verbs_comprehensive.proto
│ ├── query_params.proto
│ ├── backward_compat.proto
│ └── unwrap.proto
└── golden/
├── http_verbs_comprehensive_http.pb.go
├── http_verbs_comprehensive_http_binding.pb.go
├── query_params_http.pb.go
└── [... golden output files ...]
Test Structure#
Suite Organization:
Table-driven test pattern:
func TestLowerFirst(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
// Normal cases
{"PascalCase to camelCase", "CreateUser", "createUser"},
{"single word", "User", "user"},
{"already lowercase", "create", "create"},
// Edge cases
{"empty string", "", ""},
{"single uppercase char", "A", "a"},
{"all uppercase", "ABC", "aBC"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := lowerFirst(tt.input)
if result != tt.expected {
t.Errorf("lowerFirst(%q) = %q, expected %q", tt.input, result, tt.expected)
}
})
}
}Patterns:
-
Setup Pattern (Implicit):
- No explicit setup/teardown for unit tests
- Setup via test struct fields (table-driven tests)
- Test directory creation via
t.TempDir()for file operations
-
Teardown Pattern:
t.TempDir()automatically cleans up after test- Cleanup via
deferfor file handle closure - Explicit
defer os.Remove(pluginPath)for built artifacts
-
Assertion Pattern:
- Comparison:
if result != expected { t.Errorf(...) } - Nil checks:
if gen == nil { t.Fatal("NewGenerator returned nil") } - Error checks:
if err != nil { t.Fatalf("Failed: %v", err) } - String comparisons:
if !strings.Contains(result, expectedPart) { t.Errorf(...) }
- Comparison:
Mocking#
Framework:
- Protocol Buffers code generation reflection (protogen package)
- No external mocking library (e.g., testify/mock, GoMock)
Patterns:
Manual test stub for protogen types:
// internal/httpgen/mock_generator.go
// Contains manual mock implementations of protogen interfaces
// Used for testing generator functions with custom field/service configurationsReal protoc execution in golden file tests:
// Build the plugin binary for testing
buildCmd := exec.Command("make", "build")
buildCmd.Dir = projectRoot
if buildErr := buildCmd.Run(); buildErr != nil {
t.Fatalf("Failed to build plugin: %v", buildErr)
}
// Run protoc with the built plugin
cmd := exec.Command("protoc",
"--plugin=protoc-gen-go-http="+pluginPath,
"--go_out="+tempDir,
"--go-http_out="+tempDir,
"--proto_path="+protoDir,
protoFile,
)What to Mock:
- Use real protogen types when testing code generation (more reliable than mocks)
- Only mock when testing error conditions or behavior with specific protobuf descriptor values
What NOT to Mock:
- Protobuf descriptors (test with real .proto files)
- Code generation output (golden file tests validate real protoc output)
- External tooling like protoc (tests skip if protoc unavailable)
Fixtures and Factories#
Test Data:
Stored in testdata/proto/ directories:
internal/httpgen/testdata/proto/http_verbs_comprehensive.protointernal/tsclientgen/testdata/proto/*.protointernal/openapiv3/testdata/proto/*.proto
Pattern structure:
// Simple message for testing
message User {
string id = 1;
string name = 2;
}
// Service with HTTP configuration
service UserService {
rpc CreateUser(CreateUserRequest) returns (User) {
option (sebuf.http.config) = {
path: "/users"
method: HTTP_METHOD_POST
};
}
}Location:
- Protocol buffer test files:
internal/*gen/testdata/proto/ - Generated golden files:
internal/*gen/testdata/golden/ - Test helper functions: In the
*_test.gofile itself (e.g.,generateTestFiles())
Coverage#
Requirements:
- Threshold: 85% per package
- Coverage mode: Enabled by default with
go test - Enforced in CI via
./scripts/run_tests.sh - Fast mode: Can skip coverage for rapid feedback during development
View Coverage:
# Generate and view HTML report
go tool cover -html=coverage/coverage.out -o coverage/coverage.html
open coverage/coverage.html
# View command-line report
go tool cover -func=coverage/coverage.out
# View JSON report
cat coverage/coverage.json
# View coverage badge
cat coverage/coverage-badge.svgTest Types#
Unit Tests:
- Location:
*_test.gofiles in same package - Scope: Individual functions and helper methods
- Examples:
TestLowerFirst(): Tests name conversion functionTestCamelToSnake(): Tests case conversionTestValidationError_Struct(): Tests error struct fieldsTestIsPathParamCompatibleByKind(): Tests type compatibility logicTestHeaderNameToPropertyName(): Tests header name to property conversion
Integration Tests:
- Location:
*_integration_test.gosuffix - Scope: Multi-component interactions
- Examples:
internal/openapiv3/integration_test.go: Tests full OpenAPI document generation- Golden file tests (see below)
Golden File Regression Tests:
- Location:
*_golden_test.gofiles (e.g.,golden_test.go) - Scope: Exhaustive byte-for-byte comparison of generated code
- Pattern: Execute real protoc with built plugin, compare output to golden reference files
- Purpose: Catch ANY unintended changes to generated output
- Test files:
internal/httpgen/golden_test.go: Tests HTTP handler generationinternal/clientgen/golden_test.go: Tests Go HTTP client generationinternal/tsclientgen/golden_test.go: Tests TypeScript client generationinternal/openapiv3/exhaustive_golden_test.go: Tests OpenAPI document generation
E2E Tests:
- Not explicitly implemented
- Golden file tests serve as E2E validation (full pipeline from proto to generated code)
- Real protoc execution provides end-to-end testing
Common Patterns#
Async Testing:
- Not applicable (Go is single-goroutine for test functions)
- Some tests use
t.TempDir()which returns immediately (no async cleanup)
Error Testing:
Testing validation errors:
func TestValidationError_Struct(t *testing.T) {
err := ValidationError{
Service: "UserService",
Method: "GetUser",
Message: "path variable '{user_id}' has no matching field",
}
if err.Service != "UserService" {
t.Errorf("ValidationError.Service = %q, expected %q", err.Service, "UserService")
}
}Testing error types and nil checks:
func TestNewGenerator(t *testing.T) {
gen := openapiv3.NewGenerator(openapiv3.FormatYAML)
if gen == nil {
t.Fatal("NewGenerator returned nil")
}
if gen.Doc() == nil {
t.Error("Document is nil")
}
}Golden File Testing Pattern:
// Skip if protoc unavailable
if _, err := exec.LookPath("protoc"); err != nil {
t.Skip("protoc not found, skipping golden file tests")
}
// Define test cases with proto files and expected outputs
testCases := []struct {
name string
protoFile string
expectedFiles []string
}{
{
name: "comprehensive HTTP verbs",
protoFile: "http_verbs_comprehensive.proto",
expectedFiles: []string{
"http_verbs_comprehensive_http.pb.go",
"http_verbs_comprehensive_http_binding.pb.go",
"http_verbs_comprehensive_http_config.pb.go",
},
},
}
// For each test case:
// 1. Build plugin binary
// 2. Run protoc with built plugin
// 3. Compare generated output to golden files byte-for-byte
// 4. On mismatch:
// - Report first byte difference
// - Report line-by-line differences (first 10 only)
// - Support UPDATE_GOLDEN=1 env var to update golden files
// - Write diff to .generated file for manual inspectionGolden File Update Mechanism:
# Update golden files after intentional code generation changes
UPDATE_GOLDEN=1 go test -run TestExhaustiveGoldenFiles
# Or via Makefile
UPDATE_GOLDEN=1 go test -run TestHTTPGenGoldenFilesWhen UPDATE_GOLDEN=1:
- Mismatched golden files are overwritten with new generated content
- Logs indicate which files were updated
.generatedfiles written for comparison even on update
Golden File Helper Functions:
// reportFirstDifference: Locates first byte difference
// reportLineDifferences: Shows line-by-line diffs (limited to 10)
// handleGoldenFileUpdate: Updates golden file if UPDATE_GOLDEN=1
// writeTemporaryGeneratedFile: Writes .generated file for diff inspection
// tryCreateGoldenFile: Creates new golden file if missing and UPDATE_GOLDEN=1Test Coverage Gaps#
Known Focus Areas:
- Unit tests: Strong coverage of name conversion functions, validation logic, type mapping
- Golden file tests: Comprehensive regression detection for all code generators
- Integration tests: Limited (only in openapiv3 package)
- Error path coverage: Validation and error handling tested, but some edge cases may lack tests
Running Tests with Specific Scope:
# Run tests for single package
go test -v ./internal/httpgen
# Run tests matching pattern
go test -v -run TestLower
# Run with race detector
go test -race ./...
# Run with verbose output
go test -v ./...Testing analysis: 2026-02-05
