Skip to content

Testing & Tooling

Most languages require a third-party testing framework. Go ships everything you need inside the standard library. The testing package provides *testing.T for unit tests, *testing.B for benchmarks, and *testing.F for fuzzing — all driven by a single command: go test.

The go toolchain that ships with every Go installation also includes:

ToolPurpose
go testRun tests and benchmarks
go buildCompile packages and binaries
go runCompile and run in one step
go fmtFormat source code (wraps gofmt)
go vetStatic analysis for common mistakes
go modModule and dependency management
go docBrowse package documentation
  1. go-test — Writing _test.go files, func TestXxx(t *testing.T), table-driven tests with t.Run, t.Parallel, fuzzing with *testing.F, and everything else *testing.T gives you.
  2. benchmarks — Measuring performance with func BenchmarkXxx(b *testing.B), reading -benchmem output, and comparing runs with benchstat.
  3. modules — Managing dependencies with go mod init, go.mod, go.sum, go get, and how version resolution actually works.
  4. go-tooling — The everyday commands: go run, go build, go fmt, go vet’s bug-catching analyzers, go doc, and the wider linting ecosystem.
flowchart TD
  T["testing package"] --> A["*testing.T — unit tests, table-driven, t.Parallel"]
  T --> B["*testing.B — benchmarks, b.Loop, benchstat"]
  T --> F["*testing.F — fuzzing, mutates inputs to find edge cases"]
  A --> G["go test ./..."]
  B --> G
  F --> G
One package, one command, three ways to exercise your code

The best way to appreciate Go’s testing tools is to start with a pure function — one with no side effects, no I/O, just input and output. The playground below runs Fibonacci from main. In the next lesson you will write a proper _test.go file for exactly this kind of function — including a fuzz test, since Fibonacci and IsPrime both take a single int and are exactly the shape *testing.F is good at hammering with edge-case inputs.

package main
import "fmt"
// Fibonacci returns the nth Fibonacci number (0-indexed).
// fib(0)=0, fib(1)=1, fib(2)=1, fib(3)=2, ...
func Fibonacci(n int) int {
if n <= 1 {
return n
}
return Fibonacci(n-1) + Fibonacci(n-2)
}
// IsPrime reports whether n is a prime number.
func IsPrime(n int) bool {
if n < 2 {
return false
}
for i := 2; i*i <= n; i++ {
if n%i == 0 {
return false
}
}
return true
}
func main() {
// Print the first 10 Fibonacci numbers
fmt.Println("Fibonacci sequence:")
for i := 0; i < 10; i++ {
fmt.Printf(" fib(%d) = %d\n", i, Fibonacci(i))
}
// Find primes up to 30
fmt.Println("Primes up to 30:")
for n := 2; n <= 30; n++ {
if IsPrime(n) {
fmt.Printf(" %d\n", n)
}
}
}
What you getBenefitCost
Built-in testing packageNo install step, one standard API across every Go projectNo assertion library — you write if got != want yourself
Table-driven testsTest many cases in one function, easy to add a caseA very long table gets hard to read
go test ./...Runs every package’s tests in the module in one commandParallel tests can race on shared state if you are not careful
Race detector (-race)Catches concurrency bugs automatically2-10x slower — run it in CI, not on every local run
  • You need a testing framework like Python or Node’s — the Go standard library already has everything you need; there is no need to install testify or gomega.
  • 100% coverage means the code is correct — coverage only measures whether a line ran, not whether the behavior is correct — a test with no assertions can still reach 100% coverage.
  • go test is slow — go test caches results; if nothing changed, it returns the cached result instantly.

Before this module goes into depth, start with the single most common generated-test failure mode: a test that runs, prints PASS, and checks nothing.

  • Does every Test* function actually call t.Error/t.Errorf/t.Fatal/t.Fatalf somewhere, or does it just call the function under test and let a nil error silently mean success?
  • If the function returns a value, is that value ever compared against an expected result — or only checked for “did not panic”?
  • Does a benchmark or fuzz test’s assertion logic actually depend on the input, or would it pass identically no matter what the function under test returned?

🐛 Spot the bug

func TestParseConfig(t *testing.T) {
cfg := ParseConfig("timeout=30")
fmt.Println(cfg) // just prints — never compares to anything
}
Show the answer

This test calls ParseConfig, prints the result, and ends — there is no t.Error, t.Errorf, t.Fatal, or comparison against an expected value anywhere. go test reports PASS for this test unconditionally, whether ParseConfig returns a correct struct, an empty one, or silently drops the timeout field entirely. This compiles cleanly and “looks like a test” — which is exactly why it is dangerous: a real bug introduced later will not turn this test red, because nothing in it can ever fail. The fix is a real assertion: want := Config{Timeout: 30 * time.Second}; if cfg != want { t.Errorf("ParseConfig() = %+v, want %+v", cfg, want) }.

Which command runs all tests in the current module and every sub-package?
Which Go standard library package provides *testing.T, *testing.B, and *testing.F?
What makes a function a good candidate for a first unit test?
Will go test report PASS or FAIL for this test if ParseConfig has a bug that returns a zero-value Config?
func TestParseConfig(t *testing.T) {
    cfg := ParseConfig("timeout=30")
    fmt.Println(cfg)
}
Which tool formats Go source code according to the canonical style?
This module's lessons cover testing.T, testing.B, and testing.F. Which lesson teaches fuzzing?