
Writing and Running Tests in Go: A Practical Guide
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 testThis 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=10sThis 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 -coverTo generate a detailed HTML coverage report:
go test -coverprofile=coverage.out
go tool cover -html=coverage.outThis opens a browser window with a visual representation of which lines of code are covered by tests.
Best Practices for Testing in Go
| Practice | Description |
|---|---|
| Write tests in the same package | Test internal functions by placing tests in the same package. |
| Use table-driven tests for multiple inputs | Makes tests more readable and scalable. |
| Keep tests small and focused | Each test should test a single behavior. |
Use t.Helper() in helper functions | Helps identify the source of test failures in helper methods. |
| Write benchmarks for performance-sensitive code | Helps track performance over time. |
| Integrate testing into CI/CD pipelines | Ensures tests are run automatically on every change. |
