Testing Basics with go test
Ask a Go programmer what surprised them most moving from another language, and after "no exceptions" the next answer is often "there's nothing to install to write a test." No framework to choose between, no assertion library to argue about in a pull request, no configuration file to get right before the first test even runs. You already have everything you need, because it shipped in the same download as the compiler back in Chapter 1. This chapter is about using it.
Go treats tests the way a good workshop treats a tape measure: not a separate specialized tool bolted on afterward, but something built into the toolchain itself, sitting right next to the saw, ready to check every cut before you trust it.
Testing in Go needs no external framework to get started — go test, the
testing package, and a naming convention are the entire foundation. This
chapter covers writing and running tests, the table-driven pattern that
dominates idiomatic Go test code, and a forward look at httptest, which
Part 2 leans on heavily once networking code needs testing.
The _test.go Convention
Go recognizes any file ending in _test.go as a test file, excluded from
normal go build output but compiled and run by go test. Given a file
math.go:
package calc
func Add(a, b int) int {
return a + b
}
Its test lives alongside it, in math_test.go:
package calc
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)
}
}
Three conventions matter here, all enforced or checked by the toolchain:
the file must end in _test.go, the test function must start with
Test and take a single *testing.T parameter, and — very commonly
missed — the function name after Test must start with a capital letter
or the toolchain won't recognize it as a test at all (Testadd is silently
ignored; TestAdd is not).
Run every test in the current package with:
go test ./...
ok example.com/calc 0.002s
There's something quietly satisfying about that one-line ok — it's the
same feeling as the first Hello, Go! back in Chapter 1, just now backed
by an actual claim about your code's correctness instead of a print
statement.
t.Errorf records a failure and lets the rest of the test function keep executing — useful when later assertions are independent and you want to see all of them fail at once. t.Fatalf records the failure and immediately stops the current test function, which matters when a later line would panic on bad data from an earlier, already-failed step (for instance, dereferencing a pointer that a failed setup step left nil).Table-Driven Tests
The dominant pattern in idiomatic Go testing is the table-driven test:
a slice of input/expected-output cases, run through the same test body in
a loop. It scales far better than writing a separate TestX_CaseY
function per scenario:
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
expected int
}{
{"positives", 2, 3, 5},
{"negatives", -2, -3, -5},
{"zero", 0, 0, 0},
{"mixed signs", -5, 10, 5},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := Add(tc.a, tc.b)
if got != tc.expected {
t.Errorf("Add(%d, %d) = %d, want %d",
tc.a, tc.b, got, tc.expected)
}
})
}
}
t.Run registers each case as a named subtest, which gives you three
practical benefits at once: each case's pass/fail status is reported
individually in verbose output, a single case's failure doesn't hide the
others, and -run (below) can target one specific case by name.
A table-driven test turns "did I test enough cases?" into "did I add enough rows?" — the hard part of testing becomes enumerating scenarios, not writing boilerplate to exercise each one.
Useful go test Flags
go test -v ./... # verbose: show every test/subtest name and result
go test -run TestAdd ./... # run only tests matching this regex
go test -cover ./... # report the percentage of code exercised
-run matches against the full test name, including subtest names joined
with / — so go test -run TestAdd/negatives runs only that one row of
the table above, which becomes invaluable once a package accumulates
dozens of table-driven cases and you're chasing one specific failure.
go test -v -run TestAdd/negatives ./...
=== RUN TestAdd
=== RUN TestAdd/negatives
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/negatives (0.00s)
PASS
ok example.com/calc 0.002s
go test -cover reports what fraction of lines executed during testing, not whether the assertions on those lines were meaningful. A test that calls a function and checks nothing about its result can reach 100% coverage on that function while verifying absolutely nothing. Treat coverage as a tool for finding untested code, not as proof that tested code is correct.Testing Error Paths
Idiomatic Go tests exercise both the success path and the failure path of
any function that returns an error, using the tools from the previous
chapter:
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}
func TestDivide(t *testing.T) {
if _, err := Divide(10, 0); err == nil {
t.Error("Divide(10, 0) succeeded, want an error")
}
got, err := Divide(10, 2)
if err != nil {
t.Fatalf("Divide(10, 2) returned unexpected error: %v", err)
}
if got != 5 {
t.Errorf("Divide(10, 2) = %v, want 5", got)
}
}
Note the t.Fatalf on the second check: if Divide(10, 2) unexpectedly
errored, continuing on to compare got against 5 would just produce a
confusing second failure about the wrong thing (0, the zero value, not
matching 5) — stopping immediately keeps the failure message focused on
the real problem.
A Forward Look: httptest
Part 2 of this book builds real TCP and HTTP servers, and testing those
without a real network round trip is exactly what the standard library's
net/http/httptest package is for. A brief preview, since you'll meet it
properly once networking code needs verifying:
func TestHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/hello", nil)
rec := httptest.NewRecorder()
helloHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
if body := rec.Body.String(); body != "hello\n" {
t.Errorf("body = %q, want %q", body, "hello\n")
}
}
httptest.NewRequest builds a fake, in-memory *http.Request with no
real socket involved, and httptest.NewRecorder captures whatever an
http.HandlerFunc writes as if it were a real http.ResponseWriter —
letting you test an HTTP handler's exact logic at full speed, with no
listening port, no real connection, and no flakiness from the network at
all. Hold onto that phrase — "no flakiness from the network" — because
you'll come to appreciate it a great deal once Part 2 shows you how many
things can quietly go wrong across a real one.
A Word on Benchmarks
Alongside TestXxx, the testing package recognizes functions named
BenchmarkXxx(b *testing.B), run with go test -bench. A benchmark
repeatedly calls the code under test, letting the framework decide b.N
— how many iterations to run — until it has a stable timing measurement:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
go test -bench=. -benchtime=1x ./...
Benchmarks are not part of the everyday workflow the way tests are — you reach for them specifically when comparing two implementations for performance, not as a routine part of every package. They're mentioned here mainly so the name and shape look familiar the first time you meet one in the standard library's own source.
One more test-shaped function worth knowing: ExampleXxx() functions with
a trailing // Output: comment are compiled, executed, and checked by
go test, which compares everything printed to stdout against the text in
the comment:
func ExampleAdd() {
fmt.Println(Add(2, 3))
// Output: 5
}
If Add(2, 3) ever printed anything other than 5, this example would
fail exactly like a regular test — the difference is that these examples
also render directly in generated documentation (go doc, pkg.go.dev), so
they double as living, verified usage samples rather than static prose
that can silently drift out of date as the code changes underneath it.
| Concept | Purpose |
|---|---|
_test.go file, TestXxx(t *testing.T) |
The unit go test discovers and runs |
t.Errorf |
Record a failure, keep running the rest of the test |
t.Fatalf |
Record a failure, stop the current test function immediately |
t.Run(name, func(t *testing.T){...}) |
Register a named, individually reportable subtest |
| Table-driven test | A slice of named cases run through one shared test body |
-run, -v, -cover |
Target specific tests, show detail, measure coverage |
httptest.NewRequest/NewRecorder |
Test HTTP handlers without a real network connection |
Try It Yourself
Take the Divide function above and turn its test into a proper
table-driven test with an error-expectation column:
func TestDivideTable(t *testing.T) {
cases := []struct {
name string
a, b float64
want float64
wantErr bool
}{
{"even division", 10, 2, 5, false},
{"fractional result", 7, 2, 3.5, false},
{"divide by zero", 10, 0, 0, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := Divide(tc.a, tc.b)
if tc.wantErr && err == nil {
t.Fatalf("Divide(%v, %v) succeeded, want error",
tc.a, tc.b)
}
if tc.wantErr {
return
}
if err != nil {
t.Fatalf("Divide(%v, %v) unexpected error: %v",
tc.a, tc.b, err)
}
if got != tc.want {
t.Errorf("Divide(%v, %v) = %v, want %v",
tc.a, tc.b, got, tc.want)
}
})
}
}
- Add the table-driven test above alongside
Divide, rungo test -v ./..., and confirm all three named subtests pass. - Run
go test -run TestDivideTable/divide_by_zero -v ./...— note thatgo testreplaces spaces in subtest names with underscores when matching against-run. - Deliberately break
Divide(returna / beven whenb == 0, letting it produce+Infinstead of an error) and re-run the tests — confirm the "divide by zero" subtest fails with a clear message pointing at the missing error. - Bonus: add
go test -cover ./...to see the coverage percentage, then add a case exercising a negative divisor and note whether the percentage changes at all — a good illustration of coverage measuring lines, not scenarios.
A test you didn't write for the failure path isn't half a test suite — for error-returning functions, it's arguably the more important half you skipped.
Frequently Asked Questions
Do I need a separate _test.go file for every source file, or can one test file cover a whole package?
Nothing in the toolchain forces a one-to-one mapping between math.go and math_test.go — that convention exists purely for humans to find the right test quickly, not because go test requires it. You could legally dump every test in the package into one giant all_test.go, but splitting tests to mirror the source file they exercise is what every idiomatic Go codebase does, and it's worth adopting from your very first package.
Why did my test function silently not run at all?
The single most common cause is a lowercase letter right after Test — func Testadd(t *testing.T) compiles fine and simply never gets discovered, because the toolchain's naming rule requires an uppercase letter immediately following the word Test. The second most common cause is forgetting the *testing.T parameter or misspelling the file suffix _test.go; go test -v ./... is the fastest way to confirm exactly which tests it thinks exist.
Should I use t.Error or t.Fatal inside a table-driven test's t.Run closure?
Prefer t.Fatalf the moment a later assertion in that same subtest would be meaningless without the earlier one succeeding — exactly the Divide example above, where comparing an unexpected-error result against 5 produces a confusing second failure. t.Errorf is right when two checks in the same subtest are genuinely independent and you'd want to see both fail together rather than stopping at the first.
Is -cover something I should chase toward 100%?
Not on its own — this chapter's warning about coverage measuring lines, not scenarios, is worth taking seriously rather than treating as a throwaway caveat. A far better habit is watching which lines -cover reports as untested and asking whether that gap is a real failure path you forgot, rather than treating the percentage itself as a score to maximize.
How does this chapter's testing style carry forward into Part 2?
Every technique here — table-driven cases, testing both success and error paths, t.Run subtests — applies unchanged once your functions start doing networking instead of arithmetic. The one new tool Part 2 leans on constantly is httptest, previewed above, precisely because a real TCP or HTTP round trip is exactly the kind of slow, flaky dependency a good unit test avoids.
Where This Goes From Here
You now have the last piece of the everyday Go workflow: write code,
go run it while you're shaping it, go build it once it's worth
keeping, and go test it so "it compiles" and "it works" stop being the
same claim. That loop — write, run, test — is the same loop you'll use for
every remaining chapter in this book, just aimed at increasingly
networked code instead of toy calc packages.
There's one more idea from this part still waiting, and it's the one that made Go the language of choice for the exact kind of software this whole book is about: goroutines and channels, Go's built-in tools for doing many things at once without the usual tangle of manual thread management. Every networking server you build from Part 2 onward will juggle many connections simultaneously — this next, final chapter of Go Fundamentals is what makes that juggling feel almost ordinary instead of terrifying.