Testing in Go
Reviewed & published by Brayan K
By the end of this lesson you'll write real Go tests with the built-in testing package — single tests, table-driven tests with subtests, and benchmarks — and run them with go test. No third-party libraries needed; it's all in the standard library.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Put tests in _test.go files and write func TestXxx(t *testing.T)
- Report failures with t.Errorf (keep going) and t.Fatalf (stop now)
- Write table-driven tests and run each case as a t.Run subtest
- Run tests with go test, -v, -run, and measure with -cover
- Add a benchmark with Benchmark + *testing.B and the -bench flag
- Avoid classic traps like comparing slices or maps with ==
1️⃣ Your First Test
Go's test runner is built into the language — there's no framework to install. Three conventions are all you need: the file name ends in _test.go, each test function is named Test + a capitalised name, and it takes a single t *testing.T. Go has no assert keyword: you compare the values yourself and call a method on t when something's wrong. Read this worked example, then run it.
// math.go — the code you want to test.
package mathutil
func Add(a, b int) int {
return a + b
}
// math_test.go — tests MUST live in a file ending in _test.go,
// in the same package. The compiler treats _test.go files specially:
// they are only built when you run "go test", never in your app.
package mathutil
import "testing"
// Rules for a test function:
// - name starts with Test followed by a Capital letter (TestAdd)
// - it takes exactly one argument: t *testing.T
func TestAdd(t *testing.T) {
got := Add(2, 3) // call the thing under test
want := 5 // what you expect
// There is no assert keyword in Go. You compare yourself and,
// on failure, report it via t. t.Errorf marks the test failed
// but keeps running the rest of the function.
if got != want {
t.Errorf("Add(2, 3) = %d; want %d", got, want)
}
}2️⃣ Reporting Failures: t.Errorf vs t.Fatalf
When a check fails you tell the test runner with a method on t. t.Errorf records the failure but keeps running the rest of the function, so one run can surface several issues. t.Fatalf records it and stops the test right there — use it after setup or an error check, when the lines below would otherwise crash. Both take a printf-style format string.
package mathutil
import "testing"
func Divide(a, b int) int { return a / b }
func TestDivide(t *testing.T) {
// t.Fatalf reports the failure AND stops this test immediately
// (via runtime.Goexit). Use it when continuing makes no sense —
// here, if b is 0 the next line would panic, so we bail out first.
b := 2
if b == 0 {
t.Fatalf("b is 0; cannot divide") // stops the test now
}
got := Divide(10, b)
// t.Errorf reports the failure but KEEPS GOING, so one run can
// surface several problems at once.
if got != 5 {
t.Errorf("Divide(10, 2) = %d; want 5", got)
}
}Now you try. The test below is almost complete — fill in the two blanks marked ___ using the hints, then run it. You're writing the expected value and the comparison.
package mathutil
import "testing"
func Double(n int) int { return n * 2 }
// 🎯 YOUR TURN — finish this test, then run "go test -v".
func TestDouble(t *testing.T) {
got := Double(4)
want := ___ // 👉 the number you expect Double(4) to return
// 👉 compare got and want; on mismatch call t.Errorf with a message
if got ___ want { // 👉 use the "not equal" operator here
t.Errorf("Double(4) = %d; want %d", got, want)
}
}
// ✅ Expected output when correct:
// === RUN TestDouble
// --- PASS: TestDouble (0.00s)
// PASS3️⃣ Table-Driven Tests with Subtests
This is the pattern you'll write most in Go. Instead of copy-pasting a near-identical test for every input, you describe the cases as data in a slice of structs, then loop over them. Wrapping each case in t.Run(name, func) turns it into a named subtest, so the report tells you precisely which case broke and you can re-run just that one. Adding a new case is a single line.
package mathutil
import "testing"
// Table-driven tests are Go's signature pattern: describe each case
// as DATA in a slice, then loop and run each one as a named subtest.
func TestAdd(t *testing.T) {
// An anonymous struct describes the shape of one test case.
tests := []struct {
name string // a label for this case (shows up in -v output)
a, b int // inputs
want int // expected result
}{
{"two positives", 2, 3, 5},
{"two negatives", -1, -2, -3},
{"with zero", 0, 7, 7},
{"mixed signs", -5, 10, 5},
}
for _, tt := range tests {
// t.Run starts a SUBTEST named after the case. Each subtest
// passes or fails on its own, so you see exactly which case broke.
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, got, tt.want)
}
})
}
}Your turn again. The table below tests a Max function but is missing a case. Add one row that covers two equal numbers, then run it.
package mathutil
import "testing"
func Max(a, b int) int {
if a > b {
return a
}
return b
}
func TestMax(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"a bigger", 9, 4, 9},
{"b bigger", 2, 8, 8},
// 🎯 YOUR TURN — add ONE more case to the table below.
// 👉 a case where both numbers are equal, e.g. 5 and 5.
___, // 👉 {"name", a, b, want} — fill in all four fields
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Max(tt.a, tt.b); got != tt.want {
t.Errorf("Max(%d, %d) = %d; want %d",
tt.a, tt.b, got, tt.want)
}
})
}
}
// ✅ Expected: your new subtest appears and passes, e.g.
// --- PASS: TestMax/equal (0.00s)4️⃣ Running Tests: go test, -v, -run, -cover
You drive everything from the command line. go test runs the package's tests and prints ok or FAIL. Add -v for a line per test, -run to filter by a name regex (great with subtests), and -cover for the percentage of your code the tests exercise. ./... means "this package and everything below it".
# Run every test in the current package
go test
# -v: verbose — show each test and subtest as it runs
go test -v
# -run: only tests whose name matches a regex.
# "TestAdd" runs TestAdd; "TestAdd/zero" targets one subtest.
go test -run TestAdd
go test -run 'TestAdd/with_zero' -v
# -cover: what percentage of statements your tests exercise
go test -cover
# ./... means "this package and every package below it"
go test ./...
# Combine them freely:
go test -v -run TestMax -cover ./...5️⃣ Benchmarks (a quick note)
Once your code is correct, you may want to know if it's fast. A benchmark is named Benchmark + capital, takes a b *testing.B, and runs your code in a for i := 0; i < b.N; i++ loop. The framework raises b.N until the timing is stable. Benchmarks don't run with a plain go test — you opt in with -bench.
package mathutil
import "testing"
// A BENCHMARK measures speed. Name it Benchmark + Capital, take a
// *testing.B, and loop exactly b.N times. The framework keeps raising
// b.N until the timing is stable, then reports the per-operation cost.
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
// Benchmarks DON'T run with a plain "go test" — you opt in with -bench.
// go test -bench=. run all benchmarks
// go test -bench=. -benchmem also report memory per operationPro Tips
- 💡 Name failure messages well: the convention is "Add(2, 3) = %d; want %d" — show the call, the result you got, and what you want.
- 💡 Compare slices/maps with reflect.DeepEqual (or slices.Equal / maps.Equal in Go 1.21+) — never with ==.
- 💡 Run go test -race ./... in CI — the race detector catches concurrency bugs that only appear under load.
- 💡 Use t.Helper() inside an assertion helper so failures point at the calling test, not at the helper.
Common Errors (and the fix)
- "no test files": your file isn't named _test.go. A name like math-test.go or tests.go is ignored by go test. Rename it to end in exactly _test.go.
- Test silently never runs: the function name must be Test + a Capital letter. func testAdd or func Testadd won't be picked up — use func TestAdd.
- "invalid operation: ... (slice can only be compared to nil)": you used == on slices or maps. Use reflect.DeepEqual(got, want) instead.
- "TestXxx has wrong signature": a test must take exactly t *testing.T — not *testing.B and no extra parameters.
- Benchmark "ran 0 times": you looped a fixed number instead of b.N. The loop must be for i := 0; i < b.N; i++.
📋 Quick Reference — Go Testing
| Task | Code / Command |
|---|---|
| Test file name | something_test.go |
| Test function | func TestX(t *testing.T) |
| Fail, keep going | t.Errorf("got %d; want %d", g, w) |
| Fail, stop now | t.Fatalf("setup failed: %v", err) |
| Subtest | t.Run(name, func(t *testing.T){…}) |
| Run all / verbose | go test · go test -v |
| Run one (sub)test | go test -run 'TestX/case' |
| Coverage | go test -cover ./... |
| Benchmark | go test -bench=. -benchmem |
Mini-Challenge: Test a String Reverser
No blanks this time — just a brief and an outline. The Reverse function is written for you; write a table-driven test for it from scratch, run it, and check each case passes. This is exactly the kind of test you'll write every day in real Go projects.
package strutil
import "testing"
// The function under test (already written):
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// 🎯 MINI-CHALLENGE: write a table-driven test for Reverse.
// 1. Name it TestReverse and take t *testing.T.
// 2. Build a []struct slice of cases: a name, an input, and a want.
// Include at least: "hello" -> "olleh", "" -> "", "a" -> "a".
// 3. Loop the cases with t.Run(tt.name, ...) as a subtest.
// 4. Inside, if Reverse(tt.input) != tt.want, call t.Errorf.
//
// ✅ Expected: go test -v shows one PASS subtest per case, e.g.
// --- PASS: TestReverse/hello (0.00s)
// --- PASS: TestReverse/empty (0.00s)
// your code here🎉 Lesson Complete!
- ✅ Tests live in _test.go files; functions are func TestXxx(t *testing.T)
- ✅ t.Errorf fails but continues; t.Fatalf fails and stops the test
- ✅ Table-driven tests describe cases as data and run each with t.Run subtests
- ✅ Drive runs with go test, -v, -run, and -cover
- ✅ Benchmarks use Benchmark + b.N and run with -bench
- ✅ Compare slices and maps with reflect.DeepEqual, never ==
- ✅ You've finished the Go course — you can now build, test, and ship production Go. Next, explore Go modules, database access, and Docker deployment.
Practice quiz
Where must Go test functions live?
- In a file named tests.go
- In any .go file with a //test comment
- In a file ending in _test.go
- In a folder called tests/
Answer: In a file ending in _test.go. The go tool only compiles _test.go files when you run go test.
What signature must a test function have?
- func TestX(t *testing.T)
- func TestX(b *testing.B)
- func testX(t *testing.T)
- func TestX() error
Answer: func TestX(t *testing.T). Name is Test + Capital, taking exactly t *testing.T.
Which call reports a failure but keeps running the rest of the test?
- t.Fatalf
- t.Skip
- panic
- t.Errorf
Answer: t.Errorf. t.Errorf records the failure and continues; t.Fatalf stops the test now.
What does t.Fatalf do after reporting the failure?
- Continues to the next line
- Stops the current test immediately
- Skips the whole package
- Retries the test
Answer: Stops the current test immediately. Use Fatalf when continuing makes no sense, e.g. before a line that would panic.
In a table-driven test, what turns each case into a named subtest?
- t.Run(tt.name, func(t *testing.T){...})
- t.Subtest(tt.name)
- go test -sub
- t.Case(tt.name)
Answer: t.Run(tt.name, func(t *testing.T){...}). t.Run starts a subtest that passes or fails on its own.
For a subtest case named "with zero", how does it appear in -v output?
- TestAdd/with zero
- TestAdd.with_zero
- TestAdd/with_zero
- TestAdd-with-zero
Answer: TestAdd/with_zero. Spaces in case names become underscores in the subtest path.
Which flag filters which tests run by a name regex?
- -only
- -run
- -filter
- -match
Answer: -run. go test -run 'TestAdd/with_zero' targets one subtest.
What does ./... mean in go test ./...?
- Only the current file
- Re-run failed tests
- The parent package only
- This package and every package below it
Answer: This package and every package below it. It recurses into all subpackages.
Do plain go test runs execute Benchmark functions?
- Yes, always
- No, you opt in with -bench
- Only with -v
- Only if named TestBenchmark
Answer: No, you opt in with -bench. Benchmarks run only with the -bench flag, e.g. go test -bench=.
How should you compare two slices in a test?
- got == want
- got.Equals(want)
- reflect.DeepEqual(got, want)
- compare(got, want)
Answer: reflect.DeepEqual(got, want). Slices are not comparable with ==; use reflect.DeepEqual or slices.Equal.
Continue this course
- Previous: Building Web Services
- Next: gRPC and Protocol Buffers — Define services in .proto files and generate type-safe gRPC clients and servers
Frequently asked questions
Why must my test file end in _test.go?
The go tool only compiles _test.go files when you run go test — they are excluded from your normal build, so test code never ships in your binary. A file named tests.go or math-test.go is treated as ordinary source, its Test functions are ignored by go test, and you will see 'no test files'. The exact suffix _test.go is required.
What is the difference between t.Errorf and t.Fatalf?
Both mark the test as failed and print a formatted message. t.Errorf keeps running the rest of the test function, so a single run can report several problems at once. t.Fatalf stops the current test immediately. Use Fatalf when continuing makes no sense — for example after a setup step or an error check fails and the following lines would panic.
Why do table-driven tests use t.Run for each case?
t.Run creates a named subtest, so each row of your table passes or fails independently and the -v output tells you exactly which case broke (e.g. TestAdd/with_zero). You can also target one case with go test -run 'TestAdd/with_zero'. Without t.Run, a single failing row just fails the whole test with no clear label.
How do I run only one test or one subtest?
Use the -run flag with a regex. go test -run TestAdd runs only TestAdd. go test -run 'TestAdd/with_zero' targets a single subtest inside it. Combine with -v to see what ran. To test every package in a project, add ./... — for example go test -run TestMax ./...
Why can't I compare two slices or maps with ==?
In Go, == only works on comparable types. Slices and maps are not comparable, so got == want is a compile error ('invalid operation: slice can only be compared to nil'). Compare them with reflect.DeepEqual(got, want), or in Go 1.21+ use slices.Equal / maps.Equal for slices and maps of comparable elements.
Do benchmarks run when I run go test?
No. A plain go test runs your Test functions but skips Benchmark functions. You opt into benchmarks with the -bench flag, e.g. go test -bench=. to run them all, and add -benchmem to also report bytes and allocations per operation.