Testing

Course 3 · Ch 4
Testing: the testing Package, Table-Driven Tests, and go test
Testing is built directly into the language and toolchain — no separate library needs installing to get started

Every chapter so far has tested code by reading fmt.Println output by eye — fine for a 5-line example, unworkable for a real program with dozens of functions. Go's standard library includes a testing package, and the go command includes a built-in test runner, go test — together, the standard way Go code gets verified automatically.

The File Naming Convention

// math.go package main func Add(a, b int) int { return a + b } // math_test.go package main import "testing" func TestAdd(t *testing.T) { result := Add(2, 3) if result != 5 { t.Errorf("Add(2, 3) = %d; want 5", result) } }

A test file must be named *_test.go (here, math_test.go, testing code in math.go) — this naming is how Go's tooling recognises test code and excludes it from a normal build. Each test function's name must start with Test and take exactly one parameter, *testing.T.

Running Tests with go test

# In the terminal: go test ./... # ok example/mathpkg 0.002s go test -v ./... # === RUN TestAdd # --- PASS: TestAdd (0.00s) # PASS # ok example/mathpkg 0.002s

go test automatically finds and runs every Test... function in *_test.go files, with no manual registration needed anywhere. -v shows each individual test's name and result, rather than just a single pass/fail summary line.

t.Errorf vs t.Fatalf

func TestDivide(t *testing.T) { result, err := Divide(10, 0) if err == nil { t.Fatalf("expected an error, got none") // stops THIS test immediately } if result != 0 { t.Errorf("expected 0, got %v", result) // records failure, keeps checking } }

t.Errorf records a failure but lets the rest of the test function keep running — useful when several independent checks are worth reporting together. t.Fatalf records a failure AND stops that test function immediately, appropriate when a later check would be meaningless or panic without the earlier one passing first (here, examining result wouldn't make sense if err turned out to be unexpectedly nil from a successful division).

Table-Driven Tests — The Idiomatic Go Pattern

func TestAdd(t *testing.T) { tests := []struct { a, b, want int }{ {2, 3, 5}, {-1, 1, 0}, {0, 0, 0}, } for _, tt := range tests { result := Add(tt.a, tt.b) if result != tt.want { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.want) } } }

Rather than writing a separate test function per case, a table-driven test declares a slice of anonymous structs (Intermediate Chapter 1's struct knowledge, combined with Fundamentals Chapter 6's range) — each one a distinct input/expected-output pair — and loops over them with one shared assertion. Adding a new test case becomes a one-line addition to the table, rather than an entirely new function.

Subtests with t.Run

for _, tt := range tests { t.Run(fmt.Sprintf("%d+%d", tt.a, tt.b), func(t *testing.T) { result := Add(tt.a, tt.b) if result != tt.want { t.Errorf("got %d; want %d", result, tt.want) } }) }

t.Run("name", func(t *testing.T) { ... }) wraps each table entry as its own named subtest — go test -v then reports each case individually (TestAdd/2+3, TestAdd/-1+1, ...) instead of one combined pass/fail for the whole table, making it immediately clear which specific input failed.

go test -run
go test -run TestAdd runs only tests whose name matches the given pattern — useful for focusing on one failing test without re-running an entire, possibly slow, test suite.
ConceptConvention
Test file namename_test.go
Test function namefunc TestXxx(t *testing.T)
Record failure, continuet.Errorf(...)
Record failure, stop this testt.Fatalf(...)
Run all testsgo test ./...
Verbose outputgo test -v ./...

Coding Challenges

Challenge 1

Write a function IsEven(n int) bool, then write a test file with TestIsEven covering at least 3 cases (an even number, an odd number, and zero) using t.Errorf for any mismatch.

📄 View solution
Challenge 2

Rewrite Challenge 1's test as a table-driven test: a slice of struct { input int; want bool } with at least 5 cases, looped over with a single shared assertion.

📄 View solution
Challenge 3

Write a function SafeDivide(a, b float64) (float64, error) returning an error for division by zero. Write a table-driven test using t.Run for named subtests, covering a normal division and a divide-by-zero case, checking both the result/error appropriately for each.

📄 View solution

Chapter 4 Quick Reference

  • name_test.go — required file naming for Go to recognise test code
  • func TestXxx(t *testing.T) — required test function signature
  • t.Errorf(...) — records a failure, test function keeps running
  • t.Fatalf(...) — records a failure, stops the test function immediately
  • Table-driven tests — a slice of struct cases looped with one shared assertion
  • t.Run("name", func(t *testing.T) {...}) — named subtests, reported individually
  • go test ./... / go test -v ./... — run all tests, verbosely
  • This completes Go Advanced (Course 3) as currently planned. A possible Chapter 5 would cover JSON and encoding/json for working with APIs.