Testing
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 examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
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.
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.
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.
3️⃣ 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.
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.
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".
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 .
Pro 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
Frequently Asked Questions
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.
🎉 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