Skip to content

Test Coverage Patterns

High test coverage (95%+) as a quality gate, achieved through complexity management and refactoring for testability.

Coverage Through Simplicity

Coverage walls signal code complexity, not test inadequacy. Refactor for testability first, then write tests. See Coverage Enforcement for threshold management.


The Coverage Wall

Coverage stalls when code complexity makes comprehensive testing impractical. The solution is refactoring for testability, not exotic test infrastructure.

Symptom: Stuck Coverage

| Package | Coverage |
| ------- | -------- |
| cmd/app | 80.0%    |  ← Stuck despite comprehensive tests
| pkg/lib | 94.7%    |
| Total   | 85.8%    |  ← Below 95% target

Root Cause: Complexity

$ gocyclo -over 15 .
cmd/app/main.go:45: main.run() complexity = 35 (limit: 15)
pkg/lib/parser.go:78: Parse() complexity = 21 (limit: 15)

Cyclomatic complexity 35 = 35 execution paths through one function.

Testing all 35 paths requires:

  • Combinatorial setup (flags × config × errors)
  • Deep mocking (file I/O at multiple levels)
  • Unmaintainable test code

Solution: Refactor for Testability

Before: Monolithic Function

func run() error {
    // Parse flags (+5 complexity)
    configPath := flag.String("config", "", "config file")
    threshold := flag.Int("threshold", 95, "coverage threshold")
    flag.Parse()

    // Load config or auto-detect (+8 complexity)
    var cfg *Config
    if *configPath != "" {
        cfg, err = loadConfigFile(*configPath)
        if err != nil {
            return err
        }
    } else {
        cfg, err = autoDetectConfig()
        if err != nil {
            return err
        }
    }

    // Apply flag overrides (+4 complexity)
    if *threshold != 95 {
        cfg.Threshold = *threshold
    }

    // Analyze files (+6 complexity)
    var results []Result
    if fileInfo.IsDir() {
        results, err = analyzeDirectory(target)
    } else {
        results, err = analyzeFile(target)
    }

    // Format output (+7 complexity)
    switch *format {
    case "json":
        outputJSON(results)
    case "table":
        outputTable(results)
    case "markdown":
        outputMarkdown(results)
    }

    // Check thresholds (+5 complexity)
    for _, r := range results {
        if r.Coverage < cfg.Threshold {
            failures++
        }
    }

    return exitCode(failures)
}

Complexity: 35 decision points in one function.

After: Extracted Functions

func run() error {
    cfg := loadConfig(configPath)
    cfg = applyFlagOverrides(cfg, flags)
    results := analyzeTarget(target, cfg)
    outputResults(results, format)
    return checkResults(results, cfg)
}

func loadConfig(path string) (*Config, error) {
    if path != "" {
        return loadConfigFile(path)
    }
    return autoDetectConfig()
}

func applyFlagOverrides(cfg *Config, flags Flags) *Config {
    if flags.Threshold != 95 {
        cfg.Threshold = flags.Threshold
    }
    return cfg
}

func analyzeTarget(target string, cfg *Config) ([]Result, error) {
    info, err := os.Stat(target)
    if err != nil {
        return nil, err
    }
    if info.IsDir() {
        return analyzeDirectory(target)
    }
    return analyzeFile(target)
}

func outputResults(results []Result, format string) {
    switch format {
    case "json":
        outputJSON(results)
    case "table":
        outputTable(results)
    case "markdown":
        outputMarkdown(results)
    }
}

func checkResults(results []Result, cfg *Config) error {
    failures := countFailures(results, cfg.Threshold)
    if failures > 0 {
        printFailureGuidance(failures)
        return fmt.Errorf("%d files below threshold", failures)
    }
    return nil
}

New run() complexity: 6 (was 35).

Each extracted function: complexity 3-8, independently testable.


Testing Strategy

Before: Combinatorial Setup

func TestRun_AllCombinations(t *testing.T) {
    // Need to test:
    // - Config from file vs auto-detect
    // - Multiple flag override scenarios
    // - File vs directory analysis
    // - JSON vs table vs markdown output
    // - Pass vs fail thresholds
    // - Error at each layer

    // Result: 50+ test cases, each with complex setup
}

After: Focused Tests

func TestLoadConfig(t *testing.T) {
    tests := []struct {
        name    string
        path    string
        want    *Config
        wantErr bool
    }{
        {"explicit file", "test.yml", expectedConfig, false},
        {"auto-detect", "", autoConfig, false},
        {"missing file", "none.yml", nil, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := loadConfig(tt.path)
            if (err != nil) != tt.wantErr {
                t.Errorf("loadConfig() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("loadConfig() = %v, want %v", got, tt.want)
            }
        })
    }
}

func TestApplyFlagOverrides(t *testing.T) {
    cfg := &Config{Threshold: 95}
    flags := Flags{Threshold: 80}

    got := applyFlagOverrides(cfg, flags)

    if got.Threshold != 80 {
        t.Errorf("applyFlagOverrides() threshold = %v, want 80", got.Threshold)
    }
}

func TestOutputResults(t *testing.T) {
    tests := []struct {
        name   string
        format string
        want   string
    }{
        {"json format", "json", `[{"file":"test.go","coverage":95}]`},
        {"table format", "table", "FILE | COVERAGE\ntest.go | 95%"},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            var buf bytes.Buffer
            outputResults(testResults, tt.format, &buf)
            if got := buf.String(); got != tt.want {
                t.Errorf("outputResults() = %v, want %v", got, tt.want)
            }
        })
    }
}

Result: Simple table-driven tests, easy to maintain, high coverage.


Coverage Breakthrough

Package Before After
cmd/app 80.0% 97.5%
pkg/lib 94.7% 99.1%
Total 85.8% 99.0%

From 85% to 99% by making code testable, not adding exotic infrastructure.

For coverage enforcement (CI, pre-commit hooks, Codecov configuration), see Coverage Enforcement.


Anti-Patterns

❌ Writing Tests for Untestable Code

// Complexity 35 - impossible to test comprehensively
func TestRun_ComplexMonolith(t *testing.T) {
    // 100+ lines of setup
    // Mocking 5 layers deep
    // Only tests 3 of 35 paths
}

✅ Refactoring Then Testing

// Complexity 6 - simple table-driven test
func TestLoadConfig(t *testing.T) {
    tests := []struct{ name, path string; want *Config }{ ... }
    // Clean, maintainable, comprehensive
}

The Lesson

Coverage walls are code smells.

  • Complexity 35 → Impossible to test comprehensively
  • Complexity 6 → Table-driven tests work

Refactor for testability first. Coverage follows.



The wall wasn't about writing more tests. It was about making code simple enough to test. Coverage reached 99% when code became testable.

Comments