Testing in Go
Learn how to write tests in Go using the built-in testing package, _test.go files, the go test command, and table-driven tests.
Introduction
Go treats testing as a first-class concern, not an afterthought bolted on with a third-party framework. The built-in testing package, combined with the go test command, gives you everything needed to write, run, and measure the coverage of tests using nothing but the standard library.
- How the testing package and _test.go files are structured.
- How to write your first test function.
- How to run tests with go test and read its output.
- How to write idiomatic table-driven tests that cover many cases concisely.
The testing Package
Go test files live alongside the code they test, in the same package, with a filename ending in _test.go. Each test is a function named TestXxx that takes a single *testing.T parameter, which you use to report failures.
package mathutil
func Add(a, b int) int { return a + b}Writing Your First Test
A test calls the code under test and uses t.Errorf (or t.Fatalf to stop immediately) to report a mismatch between the expected and actual result. A test that never calls an error-reporting method automatically passes.
package mathutil
import "testing"
func TestAdd(t *testing.T) { got := Add(2, 3) want := 5
if got != want { t.Errorf("Add(2, 3) = %d; want %d", got, want) }}Running Tests with go test
go test compiles and runs every TestXxx function in the current package (or the package(s) matched by the pattern you give it) and reports pass/fail results.
go test ./...go test -v ./mathutil # verbose output, one line per testgo test -run TestAdd # only run tests matching this nameClick Run to see what this code prints.
Table-Driven Tests
When you need to test a function against many input/output combinations, the idiomatic Go pattern is a table-driven test: a slice of test cases run through a single loop, with t.Run creating a named subtest for each one.
package mathutil
import "testing"
func TestAddTableDriven(t *testing.T) { tests := []struct { name string a, b int want int }{ {"two positives", 2, 3, 5}, {"negative and positive", -2, 5, 3}, {"both zero", 0, 0, 0}, {"two negatives", -4, -6, -10}, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { 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) } }) }}Click Run to see what this code prints.
This pattern scales beautifully: adding a new case is just one more line in the table, and t.Run gives each case its own named result, so a failure immediately tells you which specific case broke.
Common Mistakes
- Using t.Fatalf inside a goroutine launched by a test — it must be called from the test's own goroutine.
- Forgetting the _test.go filename suffix, so go test never finds the file at all.
- Writing one giant test function for many cases instead of a table-driven test with named subtests.
- Not checking error return values in tests, letting a bug hide behind an ignored error.
- Sharing mutable state between subtests in a way that makes test order matter.
Best Practices
- Name tests TestXxx and keep them in the same package as the code, in a _test.go file.
- Prefer table-driven tests for functions with several input/output combinations.
- Use t.Run to give each case a readable, individually reportable name.
- Use t.Fatalf when a failure should stop the test immediately (like a failed setup step); use t.Errorf when the test should continue checking other things.
- Run go test -cover to keep an eye on test coverage as the codebase grows.
Frequently Asked Questions
Both mark the test as failed. t.Errorf logs the failure and lets the test continue running; t.Fatalf logs the failure and stops that test immediately, which is useful after a failed setup step that makes further checks meaningless.
Not necessarily — the standard testing package is sufficient for most needs. Libraries like testify add convenience assertions and mocking, which some teams prefer, but they are optional.
Call the function, check if err != nil against what you expect (some cases should error, some should not), and only then assert on the returned value if no error was expected.
Key Takeaways
- Go tests live in _test.go files as functions named TestXxx(t *testing.T).
- go test compiles and runs all matching tests and reports pass/fail results.
- Table-driven tests are the idiomatic way to cover many input/output cases concisely.
- t.Run creates named subtests, making failures easy to pinpoint.
- Testing is built into the standard toolchain — no external framework is required to get started.
Summary
Go's built-in testing package, combined with the table-driven pattern, makes it easy to build a thorough, readable test suite without reaching for extra dependencies. Next, you will learn generics — a more recent addition to Go that lets you write functions and types that work across multiple types safely.