Mastering Go Testing: From Basics to Subtests in 3 Minutes
Source: Dev.to
Level 1: The Basic Test
A test in Go is just a function that starts with Test and takes a *testing.T.
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
We don’t need external assertion libraries—simple if statements and t.Errorf are enough.
Level 2: Table‑Driven Tests
Copy‑pasting the same test for different inputs is tedious and error‑prone.
Table‑driven tests let you define a slice of test cases and iterate over them.
func TestAdd(t *testing.T) {
tests := []struct {
a, b int
expected int
}{
{2, 3, 5},
{0, 0, 0},
{-1, 1, 0},
}
for _, tt := range tests {
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)
}
}
}
Adding a new case is just another line in the slice.
Level 3: Subtests with t.Run
Subtests give each case a distinct name, improving output and debugging.
func TestAdd(t *testing.T) {
tests := []struct {
name string // New!
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"zeros", 0, 0, 0},
{"negative", -1, 1, 0},
}
for _, tt := range tests {
// t.Run creates a subtest
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("got %d, want %d", result, tt.expected)
}
})
}
}
Why is this better?
-
Clear output / easier debugging – failures show the subtest name, e.g.
TestAdd/positive numbers. -
Granular control – run a single subtest from the command line:
go test -run TestAdd/zeros -
Parallelism – invoke
t.Parallel()inside the subtest to run cases concurrently.
Summary
- Basics: Use
t.Errorfto report failures. - Table‑driven: Organize test data in a
[]struct. - Subtests: Use
t.Runfor better reporting, selective execution, and parallelism.
Happy testing!