Go Unit Testing

Testing is built directly into Go — no external framework is needed. The testing package provides everything required to write, run, and measure tests. Go's testing conventions are simple: test files end in _test.go, test functions start with Test, and the go test command runs them all.

Testing File Structure

myapp/
├── math.go        ← the code to test
└── math_test.go   ← the test file

Test files must be in the same package as the code being tested (or a _test suffix package for black-box testing).

The Code to Test

File: math.go

package math

func Add(a, b int) int {
    return a + b
}

func Subtract(a, b int) int {
    return a - b
}

func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}

Writing Tests

File: math_test.go

package math

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5

    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

func TestSubtract(t *testing.T) {
    result := Subtract(10, 4)
    expected := 6

    if result != expected {
        t.Errorf("Subtract(10, 4) = %d; want %d", result, expected)
    }
}

Test Function Anatomy

func TestAdd(t *testing.T) {
  │      │         │
  │      │         └── testing.T provides methods to report failure
  │      └──────────── must start with "Test" followed by capital letter
  └─────────────────── func keyword

t.Errorf("message")  → marks test as failed, continues running
t.Fatalf("message")  → marks test as failed, stops this test immediately
t.Logf("message")    → logs a message (only shown on failure)

Running Tests

# Run all tests in current package
go test

# Run with verbose output (shows each test name)
go test -v

# Run a specific test by name
go test -run TestAdd

# Run tests in all packages
go test ./...

Passing output:

--- PASS: TestAdd (0.00s)
--- PASS: TestSubtract (0.00s)
PASS
ok      myapp/math   0.002s

Table-Driven Tests

Table-driven tests run the same function with many different inputs in a clean, compact format. This is the standard Go pattern for thorough testing.

package math

import "testing"

func TestAddTableDriven(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"zero plus number", 0, 5, 5},
        {"negative numbers", -3, -2, -5},
        {"mixed signs", -3, 7, 4},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

Output with go test -v:

--- PASS: TestAddTableDriven (0.00s)
    --- PASS: TestAddTableDriven/positive_numbers (0.00s)
    --- PASS: TestAddTableDriven/zero_plus_number (0.00s)
    --- PASS: TestAddTableDriven/negative_numbers (0.00s)
    --- PASS: TestAddTableDriven/mixed_signs (0.00s)

Testing Error Cases

package math

import (
    "errors"
    "testing"
)

func TestDivideByZero(t *testing.T) {
    _, err := Divide(10, 0)

    if err == nil {
        t.Error("expected an error when dividing by zero, got nil")
    }
}

func TestDivideNormal(t *testing.T) {
    result, err := Divide(10, 2)

    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if result != 5 {
        t.Errorf("Divide(10, 2) = %v; want 5", result)
    }
}

Benchmark Tests

Benchmark functions measure how fast a function runs. They start with Benchmark and receive *testing.B.

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(100, 200)
    }
}
# Run benchmarks
go test -bench=.

Output:

BenchmarkAdd-8    1000000000    0.234 ns/op

Test Coverage

Check what percentage of code is covered by tests.

# Show coverage percentage
go test -cover

# Generate a visual HTML coverage report
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

Testing Best Practices

PracticeWhy It Matters
Use table-driven testsTest many inputs without repeating test logic
Test error paths explicitlyConfirm functions fail correctly, not just succeed
Keep tests independentEach test should pass or fail on its own
Use descriptive test namesFailures clearly show which case broke
Run go test ./... before committingCatches regressions across the whole project

Key Points

  • Test files end in _test.go and test functions start with Test
  • Run tests with go test; use -v for detailed output
  • Use t.Errorf to fail and continue; use t.Fatalf to fail and stop
  • Table-driven tests are the standard Go pattern for testing multiple inputs
  • Use go test -cover to measure test coverage across the package
  • Benchmark functions measure performance using *testing.B and go test -bench=.

Leave a Comment