1. go
  2. /testing
  3. /coverage

Understanding and Measuring Test Coverage in Go

Go provides built-in support for measuring test coverage. This guide covers how to use coverage tools and improve your test coverage effectively.

Basic Coverage

Running Tests with Coverage

# Run tests with coverage
go test -cover

# Generate coverage profile
go test -coverprofile=coverage.out

# View coverage in browser
go tool cover -html=coverage.out

# View coverage in terminal
go tool cover -func=coverage.out

Coverage Profile Output

$ go tool cover -func=coverage.out
github.com/example/pkg/file.go:10:   Function1       100.0%
github.com/example/pkg/file.go:20:   Function2       75.0%
github.com/example/pkg/file.go:30:   Function3       0.0%
total:                               (statements)    82.5%

Advanced Coverage

Per-Package Coverage

// main_test.go
func TestMain(m *testing.M) {
    // Setup code
    
    // Run tests with coverage
    c := testing.Coverage()
    if c < 0.80 {
        fmt.Printf("Tests passed but coverage %.2f%% is below 80%%\n", c*100)
        os.Exit(1)
    }
    
    os.Exit(m.Run())
}

Coverage Tags

// +build coverage

package main

func init() {
    // Coverage-specific initialization
}

// Regular code...

Coverage Tools

Using go-test-coverage

# Install go-test-coverage
go install github.com/vladopajic/go-test-coverage@latest

# Configure coverage check
cat > .testcoverage.yml << EOF
coverage:
  statements: 80
  branches: 70
  functions: 60
  lines: 80
EOF

# Run coverage check
go-test-coverage

Coverage Reports

package main

import (
    "testing"
    "os"
    "fmt"
)

func TestWithCoverage(t *testing.T) {
    if os.Getenv("GENERATE_COVERAGE") == "1" {
        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("Test panicked: %v\n", r)
            }
        }()
    }
    
    // Test code...
}

Best Practices

1. Set Coverage Targets

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-go@v2
        with:
          go-version: '1.21'
      
      - name: Run Tests
        run: |
          go test -coverprofile=coverage.out ./...
          
      - name: Check Coverage
        run: |
          coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%')
          if (( $(echo "$coverage < 80" | bc -l) )); then
            echo "Coverage $coverage% is below target of 80%"
            exit 1
          fi

2. Exclude Generated Code

//go:generate go run generate.go
//go:build !coverage

package main

// Generated code here...

3. Focus on Critical Paths

func TestCriticalPath(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    string
        wantErr bool
    }{
        // Core functionality
        {"basic case", "input", "output", false},
        
        // Edge cases
        {"empty input", "", "", true},
        {"invalid input", "invalid", "", true},
        
        // Boundary conditions
        {"max length", strings.Repeat("a", maxLen), "", true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Process(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("Process() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if got != tt.want {
                t.Errorf("Process() = %v, want %v", got, tt.want)
            }
        })
    }
}

Coverage Analysis

Statement Coverage

func ExampleFunction() {
    // This line is covered
    fmt.Println("Hello")
    
    if someCondition {
        // This might not be covered
        fmt.Println("Conditional")
    }
}

func TestExampleFunction(t *testing.T) {
    ExampleFunction() // Only covers the first line
}

Branch Coverage

func ProcessValue(val int) string {
    if val < 0 {
        return "negative"
    } else if val == 0 {
        return "zero"
    } else {
        return "positive"
    }
}

func TestProcessValue(t *testing.T) {
    tests := []struct {
        name string
        val  int
        want string
    }{
        {"negative", -1, "negative"},
        {"zero", 0, "zero"},
        {"positive", 1, "positive"},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := ProcessValue(tt.val); got != tt.want {
                t.Errorf("ProcessValue() = %v, want %v", got, tt.want)
            }
        })
    }
}

Function Coverage

type Service struct {
    db Database
}

func (s *Service) ProcessItem(item *Item) error {
    // Coverage should include error paths
    if err := s.validate(item); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }
    
    if err := s.db.Save(item); err != nil {
        return fmt.Errorf("save failed: %w", err)
    }
    
    return nil
}

func TestService_ProcessItem(t *testing.T) {
    tests := []struct {
        name    string
        item    *Item
        dbErr   error
        wantErr bool
    }{
        {
            name:    "success",
            item:    &Item{Valid: true},
            wantErr: false,
        },
        {
            name:    "invalid item",
            item:    &Item{Valid: false},
            wantErr: true,
        },
        {
            name:    "db error",
            item:    &Item{Valid: true},
            dbErr:   errors.New("db error"),
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            mockDB := &MockDB{err: tt.dbErr}
            s := &Service{db: mockDB}
            
            err := s.ProcessItem(tt.item)
            if (err != nil) != tt.wantErr {
                t.Errorf("ProcessItem() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

Continuous Integration

GitHub Actions Example

name: Test Coverage

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Go
      uses: actions/setup-go@v2
      with:
        go-version: '1.21'
    
    - name: Run Tests
      run: |
        go test -race -coverprofile=coverage.out -covermode=atomic ./...
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v2
      with:
        file: ./coverage.out
        fail_ci_if_error: true

Next Steps