Writing Unit Tests

Go uses the testing package to facilitate unit testing. Any file ending in _test.go in the same package as the code being tested is considered a test file. A test function must start with the word Test, followed by a name that starts with an uppercase letter, and it must take a single argument of type *testing.T.

Here's an example of a simple function and its corresponding test:

// math.go
package mathutil

func Add(a, b int) int {
    return a + b
}
// math_test.go
package mathutil

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2,3) = %d; want 5", result)
    }
}

To run the test, use the go test command from the package directory:

go test

This will compile and run all test functions in the package.

Table-Driven Tests

For functions with multiple input/output combinations, table-driven tests provide a clean and maintainable approach. They allow you to define a slice of test cases and iterate through them in a loop.

// math_test.go
package mathutil

import "testing"

func TestAdd(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {2, 3, 5},
        {-1, 1, 0},
        {0, 0, 0},
        {100, -99, 1},
    }

    for _, tt := range tests {
        got := Add(tt.a, tt.b)
        if got != tt.want {
            t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
        }
    }
}

This approach increases test coverage and makes it easier to add new test cases.

Benchmarking

Benchmarking is used to evaluate the performance of your code. Go supports benchmark functions through the testing package as well. Benchmark functions must begin with Benchmark, take a *testing.B argument, and are run using the go test -bench flag.

// math_test.go
package mathutil

import "testing"

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

Run the benchmark with:

go test -bench=.

This will execute the benchmark and report the number of iterations per second and the time per operation.

Fuzzing

Fuzzing is a technique for finding bugs by providing invalid, unexpected, or random data as inputs to a function. Go supports fuzzing through the testing package.

// math_test.go
package mathutil

import "testing"

func FuzzAdd(f *testing.F) {
    f.Add(2, 3)
    f.Add(-1, 100)
    f.Add(0, -1)

    f.Fuzz(func(t *testing.T, a, b int) {
        got := Add(a, b)
        if got != a+b {
            t.Errorf("Add(%d, %d) = %d; want %d", a, b, got, a+b)
        }
    })
}

Fuzzing is run with the -fuzz flag:

go test -fuzz=FuzzAdd -fuzztime=10s

This will run the fuzzer for 10 seconds, generating random inputs and checking for failures.

Test Coverage

Go provides a built-in way to measure test coverage. To generate a coverage report, run:

go test -cover

To generate a detailed HTML coverage report:

go test -coverprofile=coverage.out
go tool cover -html=coverage.out

This opens a browser window with a visual representation of which lines of code are covered by tests.

Best Practices for Testing in Go

PracticeDescription
Write tests in the same packageTest internal functions by placing tests in the same package.
Use table-driven tests for multiple inputsMakes tests more readable and scalable.
Keep tests small and focusedEach test should test a single behavior.
Use t.Helper() in helper functionsHelps identify the source of test failures in helper methods.
Write benchmarks for performance-sensitive codeHelps track performance over time.
Integrate testing into CI/CD pipelinesEnsures tests are run automatically on every change.

Learn more with useful resources