Testing in Go
Testing in Go
Go ships with a built-in testing framework in the testing package — no third-party libraries required for basic testing.
Test Function Naming Test functions must:
- Live in a file ending with
_test.go - Start with
Testfollowed by a name beginning with an uppercase letter - Accept a single parameter
t *testing.T
func TestAdd(t *testing.T) { ... }Run all tests: go test ./...
Reporting Failures
t.Error(args...)/t.Errorf(format, args...)— marks the test as failed but continues executiont.Fatal(args...)/t.Fatalf(format, args...)— marks as failed and stops the current test immediatelyt.Log()/t.Logf()— records output, visible only when the test fails or with-v
Table-Driven Tests
The idiomatic Go testing pattern: define a slice of test cases, range over them, calling t.Run for each:
tests := []struct {
name string
input int
expected int
}{ ... }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := MyFunc(tt.input)
if got != tt.expected {
t.Errorf("got %d, want %d", got, tt.expected)
}
})
}Table-driven tests minimise boilerplate and make it easy to add cases.
Subtests with t.Run
t.Run(name, func) creates a named subtest, which can be run individually:
go test -run TestAdd/negative_numberBenchmark Functions
Benchmark functions start with Bench and accept b *testing.B:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}Run with go test -bench=.. The framework adjusts b.N until the benchmark runs long enough for stable measurements.
Test Coverage
go test -cover ./... # show coverage percentage
go test -coverprofile=cov.out # save to file
go tool cover -html=cov.out # open interactive HTML reporttesting.Short()
testing.Short() returns true when -short is passed. Use it to skip slow integration tests:
if testing.Short() {
t.Skip("skipping slow test in short mode")
}Example Functions
Example functions document and test simultaneously. They start with Example and include an // Output: comment that the test runner verifies:
func ExampleAdd() {
fmt.Println(Add(1, 2))
// Output:
// 3
}Examples appear in go doc output automatically.
Code Examples
package math
import (
"testing"
)
// The function under test
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// Basic test function
func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 5.0 {
t.Errorf("Divide(10, 2) = %v; want 5.0", result)
}
}
// Test the error case
func TestDivideByZero(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Fatal("expected error for division by zero, got nil")
}
}
// Example function — doubles as documentation and test
func ExampleDivide() {
result, _ := Divide(10, 4)
fmt.Printf("%.2f\n", result)
// Output:
// 2.50
}t.Fatalf stops the test immediately — use it when the test cannot continue. t.Errorf continues the test after marking it as failed. Example functions verify output automatically.
package stringutil
import (
"strings"
"testing"
)
func Palindrome(s string) bool {
s = strings.ToLower(s)
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
if runes[i] != runes[j] {
return false
}
}
return true
}
func TestPalindrome(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{"empty string", "", true},
{"single char", "a", true},
{"simple palindrome", "racecar", true},
{"case insensitive", "RaceCar", true},
{"not a palindrome", "hello", false},
{"unicode palindrome", "aba", true},
{"two chars same", "aa", true},
{"two chars diff", "ab", false},
}
for _, tt := range tests {
tt := tt // capture range variable for parallel tests
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // run subtests in parallel
got := Palindrome(tt.input)
if got != tt.want {
t.Errorf("Palindrome(%q) = %v; want %v", tt.input, got, tt.want)
}
})
}
}Table-driven tests with t.Run give each case a descriptive name visible in output. t.Parallel() allows subtests to run concurrently. The tt := tt line captures the loop variable to avoid the closure trap.
package stringutil
import (
"strings"
"testing"
)
// Two implementations to benchmark
func joinConcat(parts []string) string {
result := ""
for _, p := range parts {
result += p // new allocation on every iteration
}
return result
}
func joinBuilder(parts []string) string {
var b strings.Builder
b.Grow(len(parts) * 10) // pre-allocate hint
for _, p := range parts {
b.WriteString(p)
}
return b.String()
}
var parts = []string{"alpha", "beta", "gamma", "delta", "epsilon"}
func BenchmarkJoinConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
joinConcat(parts)
}
}
func BenchmarkJoinBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
joinBuilder(parts)
}
}b.N is automatically tuned for a statistically reliable measurement. -benchmem shows allocations per operation. strings.Builder wins by avoiding repeated string allocations in the loop.
Quick Quiz
1. What is the difference between t.Error and t.Fatal?
2. Why do table-driven tests use `t.Run` for each case?
3. What value does `b.N` represent in a benchmark function?
4. What does an Example function's `// Output:` comment do?
Was this lesson helpful?