Testing Basics — Junior¶
This page walks through the Go testing system from absolute zero. You will not see a tool more advanced than go test. By the end you should be able to: write a TestXxx function, run it, read the failure output, write a table-driven test, and add an Example that doubles as documentation. We will go slowly because the surface area is small and almost every feature you see in advanced Go projects is composed from these few primitives.
1. What is a test in Go?¶
A test is a regular Go function with a specific signature, placed in a file with a specific suffix, in the same package as the code it tests. There is no annotation, no class, no inheritance. The framework is:
- A file ending in
_test.go. - A function
func TestXxx(t *testing.T)whereXxxstarts with an uppercase letter.
That is it. The go test command finds and runs the function.
A minimal example¶
Create a directory calc/ with one file:
And one test file next to it:
// calc/calc_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)
}
}
Now from inside calc/:
That is the whole loop. No registration, no DSL. The test passes silently. To see what ran, pass -v:
When the test fails¶
Break Add deliberately:
Re-run:
$ go test
--- FAIL: TestAdd (0.00s)
calc_test.go:9: Add(2, 3) = -1, want 5
FAIL
exit status 1
FAIL calc 0.123s
The framework printed:
- The test name.
- The file and line of the failing assertion.
- The message you passed to
t.Errorf. - A non-zero exit code so CI sees the failure.
Notice we built the failure message manually. Go does not have built-in assert(got == want) because the standard advice is to print both values explicitly. A bare "assertion failed" is useless; "got -1, want 5" tells you what to do next.
Fix the bug:
and re-run. PASS.
2. The _test.go rule¶
The Go toolchain treats _test.go as a magic suffix. Such files are compiled only by go test; they are invisible to go build, go install, go run, and any tool walking the package's source files.
$ go build ./calc
# nothing happens — calc has no main, but calc_test.go is ignored
$ ls -la
calc.go calc_test.go
Three consequences:
- Your production binary contains no test code, ever.
- You can
import "testing"and other test-only packages without polluting the public dependency list. - Test helpers and fixtures live next to the code they test, not in a separate
tests/tree.
A test file can declare:
package calc— the internal test package. Has access to unexported identifiers.package calc_test— the external test package. Sees only the public API.
Both can coexist in the same directory; go test compiles them together. The middle page covers the trade-off; for now, use package calc (internal) and move on.
3. The testing.T API¶
*testing.T is the handle the framework gives every test function. The methods you will use 99% of the time:
t.Errorf(format, args...)— mark the test as failed, log a formatted message, keep running.t.Fatalf(format, args...)— same asErrorfbut stop the test immediately.t.Log(args...)/t.Logf(format, args...)— log a message (printed only on failure or with-v).t.Run(name, func)— run a subtest with its own*testing.T.t.Helper()— mark this function as a helper so failure lines point at the caller.t.Skip(args...)/t.Skipf(format, args...)— skip the test with a message.t.Cleanup(func)— register a function to run when the test ends.t.TempDir()— return a unique temp directory that is cleaned up automatically.t.Setenv(key, value)— set an env var, restored when the test ends.
We will meet every one of these. Right now, the only two you need are Errorf and Fatalf.
When to use Errorf vs Fatalf¶
The rule: if continuing the test would produce a panic or noise, use Fatalf. Otherwise use Errorf.
func TestUser(t *testing.T) {
u, err := NewUser("alice")
if err != nil {
t.Fatalf("NewUser: %v", err) // u is nil; can't continue
}
if u.Name != "alice" {
t.Errorf("u.Name = %q, want %q", u.Name, "alice")
}
if u.ID == 0 {
t.Errorf("u.ID = 0, want non-zero")
}
}
If NewUser failed, the next line would dereference a nil pointer. Fatalf stops it. The two Errorf calls are independent assertions: if the name is wrong and the ID is zero, you want both reported, not just the first.
A subtle point about Fatalf¶
Fatalf calls runtime.Goexit, which exits only the goroutine it runs in. That means you cannot call Fatalf from a goroutine other than the one running the test function — the test goroutine would never see the failure and the test would silently pass.
// WRONG
func TestServer(t *testing.T) {
go func() {
if err := serve(); err != nil {
t.Fatalf("serve: %v", err) // WRONG goroutine
}
}()
// ...
}
The correct form is to pass the error back via a channel and assert in the test goroutine, or to call t.Errorf (which is safe from any goroutine because it does not exit). We come back to this in the find-bug page.
4. Table-driven tests¶
The single most idiomatic Go test pattern is the table-driven test: define a slice of cases, loop over them, run an assertion per case.
func TestAdd(t *testing.T) {
cases := []struct {
a, b int
want int
}{
{2, 3, 5},
{-1, 1, 0},
{0, 0, 0},
{-2, -3, -5},
}
for _, tc := range cases {
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.want)
}
}
}
Three properties make this idiom good:
- New cases are one line. Coverage grows linearly with little code.
- All cases run even if one fails (because we use
Errorf, notFatalf). - Failure messages include the input, so the report identifies the case.
Improving the report with subtests¶
When the table grows, you want each case named in the output. Use t.Run:
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"positives", 2, 3, 5},
{"negative_to_zero", -1, 1, 0},
{"zero", 0, 0, 0},
{"two_negatives", -2, -3, -5},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.want)
}
})
}
}
Now -v shows each case:
=== RUN TestAdd
=== RUN TestAdd/positives
=== RUN TestAdd/negative_to_zero
=== RUN TestAdd/zero
=== RUN TestAdd/two_negatives
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/positives (0.00s)
--- PASS: TestAdd/negative_to_zero (0.00s)
--- PASS: TestAdd/zero (0.00s)
--- PASS: TestAdd/two_negatives (0.00s)
And you can run a single case:
The -run flag is a regular expression matched against the full test name. The slash separates parent from subtest.
5. Reading the failure line¶
When a test fails the framework prints something like:
The components:
--- FAIL— final status of this test (or subtest).TestAdd/negative_to_zero— full hierarchical name.(0.00s)— wall time spent in the test.calc_test.go:12— the file and line wheret.Errorfwas called.- The formatted message.
The line number is the call site of Errorf, not the line of the bug. If you wrap assertions in a helper, the line will point inside the helper unless you call t.Helper(). We will see that in middle.md.
6. The go test command¶
go test has a lot of flags. As a junior you will use a small subset:
go test # current package, no output unless failure
go test -v # verbose: print every RUN/PASS line
go test -run TestAdd # only tests matching the regexp
go test ./... # all packages under current directory
go test -count=1 # disable test cache (force re-run)
go test -timeout=30s # kill the binary if it takes longer than 30s
go test -race # run with the race detector
go test -cover # print coverage percentage
go test -failfast # stop at first failure
go test -short # set testing.Short() to true (we use this later)
Run them all once on your calc package to get a feel.
-run regexp behaviour¶
The argument to -run is a regular expression. It matches anywhere in the test name. So -run Add matches TestAdd, TestAddSubtract, TestThingAdd. To anchor: -run '^TestAdd$'. For subtests: -run 'TestAdd/zero'. The / separates levels; each level is matched independently against the regexp split on /.
Test packages¶
go test operates on one package at a time. go test (no arg) is the current directory. go test ./calc is a specific package. go test ./... expands to every package under the current directory tree. Each package gets its own ephemeral binary, so packages run independently.
7. The test binary¶
You can see the binary go test builds:
-c says "compile but do not run". -o names the output. Running the binary executes the tests:
The flags accepted by the binary are the same as go test but with a -test. prefix: -test.v, -test.run, -test.timeout. The go test command translates the user-facing flags into the binary form. This matters when you debug a test binary in dlv or run it manually under strace.
8. Example functions¶
Examples are runnable documentation. They look like tests but have a special signature and a magic comment.
package calc_test
import (
"fmt"
"calc" // adjust import path for your module
)
func ExampleAdd() {
fmt.Println(calc.Add(2, 3))
// Output: 5
}
What happens:
go testfindsExampleAddby name.- It runs the function, capturing
os.Stdout. - It compares the captured output line-by-line (after trimming trailing whitespace) to the
// Output:comment block. - If they match, the example passes. If not, it fails like a normal test.
go doc calc.Addandpkg.go.devrender the example next to the function's documentation.
A few rules:
- An
Examplewith no// Output:comment is compiled but not run. (Useful when stdout would be too noisy or non-deterministic.) // Output:and the lines below must immediately precede the function's closing brace.// Unordered output:compares the lines as an unordered set — useful for maps and other random-order outputs.
Naming examples¶
The function name determines what doc symbol the example attaches to:
ExampleAdd— attached toAdd.ExampleAdd_negative— sub-example ofAdd.Example— package-level example.Example_overview— package-level example with a tag.
In go doc calc, all of these appear under the relevant header.
Examples vs tests¶
An Example is also a test in that it fails the suite when its output disagrees. But its primary purpose is documentation. Use examples for the happy path; use tests for edge cases. Do not write 20 examples; write the one that demonstrates the typical use, then test the edges with TestXxx.
9. The testing.T.Log family¶
Sometimes you want to log diagnostic information without failing the test. t.Log and t.Logf do this:
func TestX(t *testing.T) {
payload := buildPayload()
t.Logf("payload size: %d bytes", len(payload))
if err := send(payload); err != nil {
t.Errorf("send: %v", err)
}
}
The log is buffered and printed only if the test fails or if -v is set. This keeps a passing suite quiet.
t.Log is useful for:
- Confirming what input a test used.
- Recording timing or sizes for later inspection.
- Annotating failure context when many small assertions might trigger.
Avoid fmt.Println in test code — it always prints and clutters CI logs.
10. Setup and teardown — the simplest form¶
Many tests need a small amount of setup. The simplest pattern is local: do the work inside the test function.
This works because defer runs when the function returns. The pattern breaks down when:
- The setup must run once per test binary, not per test.
- The test launches goroutines that outlive the function body.
- The cleanup needs to happen after parallel subtests complete.
For those cases we use TestMain and t.Cleanup, which the middle page covers in depth. For now, defer is fine.
11. TestMain — once per package¶
If you need setup that applies to the whole test binary, declare a TestMain function:
package calc
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
// setup before any test runs
setupDatabase()
code := m.Run() // runs all the TestXxx functions, returns 0 if all pass
teardownDatabase()
os.Exit(code)
}
Key points:
TestMainreplaces the auto-generatedmainof the test binary. The framework calls it.- You must call
m.Run()to actually run the tests. - You must
os.Exit(code)with the return value ofm.Run()— otherwise the binary exits with code 0 regardless of test results. - There can be at most one
TestMainper package.
If you do not define TestMain, the framework auto-generates one that just calls m.Run() and exits with its code. So TestMain is purely a hook for cross-cutting setup.
12. A complete first test file¶
Putting it all together:
// calc/calc.go
package calc
import "errors"
func Add(a, b int) int { return a + b }
func Div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// calc/calc_test.go
package calc
import (
"fmt"
"testing"
)
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"positives", 2, 3, 5},
{"negative_to_zero", -1, 1, 0},
{"zero", 0, 0, 0},
{"two_negatives", -2, -3, -5},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.want)
}
})
}
}
func TestDiv_Success(t *testing.T) {
got, err := Div(10, 2)
if err != nil {
t.Fatalf("Div(10, 2) returned error: %v", err)
}
if got != 5 {
t.Errorf("Div(10, 2) = %d, want 5", got)
}
}
func TestDiv_ZeroDenominator(t *testing.T) {
_, err := Div(10, 0)
if err == nil {
t.Fatalf("Div(10, 0) returned nil error, want non-nil")
}
}
func ExampleAdd() {
fmt.Println(Add(2, 3))
// Output: 5
}
Run it:
$ go test -v
=== RUN TestAdd
=== RUN TestAdd/positives
=== RUN TestAdd/negative_to_zero
=== RUN TestAdd/zero
=== RUN TestAdd/two_negatives
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/positives (0.00s)
--- PASS: TestAdd/negative_to_zero (0.00s)
--- PASS: TestAdd/zero (0.00s)
--- PASS: TestAdd/two_negatives (0.00s)
=== RUN TestDiv_Success
--- PASS: TestDiv_Success (0.00s)
=== RUN TestDiv_ZeroDenominator
--- PASS: TestDiv_ZeroDenominator (0.00s)
=== RUN ExampleAdd
--- PASS: ExampleAdd (0.00s)
PASS
ok calc 0.123s
You now have everything a Go program needs to be tested: unit assertions, table-driven coverage, named cases, and a runnable documentation example.
13. Common newcomer mistakes¶
A short list of things that confuse people coming from JUnit, RSpec, or pytest:
"Where is the assertion library?"¶
There isn't one. You write if got != want { t.Errorf(...) }. The reason: explicit comparison makes the failure message useful (got 3, want 5 instead of assertion failed). For complex types, use reflect.DeepEqual or github.com/google/go-cmp/cmp — we'll cover both later. There is also the community library testify, but the Go standard recommendation is to avoid dependency-heavy assertion frameworks.
"How do I run a specific test?"¶
go test -run TestName. The argument is a regular expression. For a subtest: go test -run 'TestParent/sub_name'.
"Why do I need _test.go suffix?"¶
To keep test code out of your production binary. The Go toolchain refuses to compile _test.go files except via go test. This is a feature.
"Why don't I have a setUp method?"¶
You have TestMain (once per binary) and t.Cleanup (once per test). Per-test setup is just code at the top of the test function. Most Go developers prefer the explicit local style.
"Why do my parallel tests share data?"¶
If you call t.Parallel() in a subtest closure that captures a loop variable, before Go 1.22 the closure sees the loop variable's final value. Fix with tc := tc or upgrade to Go 1.22+. Even with the fix, parallel tests share process state: globals, env, file system. Plan accordingly.
"Why does my test pass even when the assertion is wrong?"¶
Three likely causes:
- You used
t.Loginstead oft.Error/t.Fatal.Lognever fails. - You called
t.Fatalfrom a goroutine other than the test goroutine. - The test is cached. Try
-count=1.
14. What to do next¶
You can now write basic Go tests. The middle page introduces:
- The internal-vs-external package choice.
- Subtests with
t.Run, deeper than the brief intro here. - Parallel tests (
t.Parallel). t.Cleanup,t.TempDir,t.Setenv— the three lifecycle helpers.t.Helperfor clean failure lines.t.Skipfor conditional skipping.
Before moving on, do the first three tasks in tasks.md. They take ten minutes total and burn the basic patterns into muscle memory.
15. Glossary¶
- Test function: A function
func TestXxx(t *testing.T)in a_test.gofile. Run bygo test. - Subtest: A nested test created by
t.Run(name, func). Has its own*testing.T. - Internal test package: A
_test.gofile declaringpackage foo— sees unexported identifiers. - External test package: A
_test.gofile declaringpackage foo_test— sees only exported identifiers. - Example function:
func ExampleXxx()with a// Output:comment. Verified runnable documentation. - TestMain: Optional
func TestMain(m *testing.M)for per-binary setup and teardown. -run:go testflag selecting tests by regexp on the test name.-v:go testflag for verbose output. Prints everyRUN/PASSline.-count=1:go testflag that disables the test cache for this invocation.t.Errorf: Mark the test as failed, continue running.t.Fatalf: Mark the test as failed and exit the test goroutine.t.Log/t.Logf: Log a message. Visible on failure or with-v.
That's all you need to start. Move on to middle.md.