Skip to content

Intermediate · Packages, testing and generics · topic 23 of 46 · 40 min

Testing with go test

Explanation

Go has testing built in. Put tests in files ending in _test.go, write functions named TestXxx(t *testing.T), and run go test ./... Go developers write table-driven tests: a list of inputs and expected outputs checked in one loop, with t.Run giving each case a name. -cover shows how much code the tests run, and -race finds data races.

Why test?

A test is code that runs your code and checks the result. Once written, it runs in seconds, every time you change something. Tests catch bugs before users do, let you change code without fear, and are expected in any professional Go job and portfolio project.

Your first test

go
// gst.go
package gst

func WithGST(paise int64) int64 {
	return paise + paise*18/100
}
The code to test.
go
// gst_test.go
package gst

import "testing"

func TestWithGST(t *testing.T) {
	got := WithGST(10000)
	want := int64(11800)
	if got != want {
		t.Errorf("WithGST(10000) = %d; want %d", got, want)
	}
}
The file name ends in _test.go; the function name starts with Test and takes *testing.T.
bash
go test ./...
# ok      example.com/billing/gst   0.004s

go test -v ./...      # show each test name
go test -run WithGST  # only tests whose name matches the pattern
Test files are compiled only by go test, never into your normal binary.
  • t.Errorf reports a failure and the test continues, so you see several problems at once.
  • t.Fatalf reports a failure and stops this test immediately. Use it when continuing makes no sense (for example the setup failed).
  • Write messages as "Func(input) = got; want expected", so a failure tells the whole story.
  • Go has no assert library in the standard library on purpose: plain if statements are enough. Some teams use github.com/stretchr/testify, which is popular but optional.

Table-driven tests

Instead of one test function per case, Go developers list the cases in a slice of structs and loop over them. Adding a case is one line. t.Run runs each case as a named subtest, so a failure says exactly which case broke.

go
func TestWithGST(t *testing.T) {
	tests := []struct {
		name string
		in   int64
		want int64
	}{
		{"zero", 0, 0},
		{"hundred rupees", 10000, 11800},
		{"one paisa rounds down", 1, 1},
		{"big order", 1_000_000, 1_180_000},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if got := WithGST(tc.in); got != tc.want {
				t.Errorf("WithGST(%d) = %d; want %d", tc.in, got, tc.want)
			}
		})
	}
}
Run one case with go test -run "TestWithGST/big_order" (spaces in subtest names become underscores). Underscores in numbers like 1_000_000 are just for readability.

Coverage and the race detector

bash
go test -cover ./...
# ok  example.com/billing/gst  0.003s  coverage: 100.0% of statements

go test -coverprofile=cover.out ./...
go tool cover -html=cover.out     # opens a coloured report in the browser

go test -race ./...               # find data races (module 6)
Coverage shows which lines tests run; it does not prove the checks are good.
Careful: Chasing 100% coverage is a trap. Aim to test the important logic and the tricky edge cases (zero, negative, empty, very large, wrong input). On some systems, such as Windows, -race also needs a C compiler (cgo); if it complains, run it in Linux, WSL or CI.

Useful helpers

HelperUse
t.Helper()Call at the top of your own check function so failures point at the caller's line
t.TempDir()A temporary folder deleted automatically after the test
t.Cleanup(func(){...})Register clean-up that runs when the test ends
t.Parallel()Let this test run in parallel with other parallel tests
t.Skip("reason")Skip a test, for example when an environment variable is missing
testdata/ folderFiles for tests; Go tools ignore it when building
net/http/httptestTest HTTP handlers without a real server (module 8)

Example functions

go
func ExampleWithGST() {
	fmt.Println(WithGST(10000))
	// Output: 11800
}
An Example function is run by go test and its printed output is compared with the // Output: comment. It also appears in the documentation.
Tip: Black-box tests: name the test package gst_test (with the suffix) to test only the exported API, exactly like a user of your package would.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Testing with go test · 1/7

go test

Go has testing built in. No extra framework is needed.

Prefer a video by a teacher? Find free videos on “golang table driven tests t.Run go test cover tutorial” on YouTube ↗

In real life: Testing a ticket fare calculator

A bus booking startup had a bug: senior citizens were charged full fare on night buses. The fix came with a table-driven test listing every combination of age group and bus type. Now any future change that breaks a fare rule fails in CI within a minute, before it reaches passengers.

go
func TestFare(t *testing.T) {
	tests := []struct {
		name  string
		age   int
		night bool
		want  int
	}{
		{"adult day", 30, false, 500},
		{"adult night", 30, true, 600},
		{"senior day", 65, false, 250},
		{"senior night", 65, true, 300},
		{"child", 8, false, 250},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if got := Fare(tc.age, tc.night); got != tc.want {
				t.Errorf("Fare(%d, %v) = %d; want %d", tc.age, tc.night, got, tc.want)
			}
		})
	}
}
The fares are made-up; the table shape is the point.

Practice (0/4 done)

Task 1 · Question

Which is a valid test function that go test will run?

Task 2 · Question

What is the difference between t.Errorf and t.Fatalf?

Task 3 · Question

Which file name will go test pick up?

Task 4 · Practice

Write func IsValidPIN(pin string) bool (6 digits, first digit not 0) and a table-driven test with at least 6 cases, including empty string, 5 digits, a letter, and a leading zero. Run it with go test -v -cover.

Quick quiz

Question 1

1.What does t.Run do?

Question 2

2.Does go build include _test.go files in your binary?

Question 3

3.What does go test -cover report?

Question 4

4.Why call t.Helper() in a check function?

Question 5

5.Which flag finds data races in concurrent code?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does t.Run do?

Answer: Runs a named subtest

t.Run creates a subtest with its own name and result.

Does go build include _test.go files in your binary?

Answer: No

Test files are compiled only by go test.

What does go test -cover report?

Answer: Percentage of statements run by the tests

Coverage is the share of statements executed during the tests.

Why call t.Helper() in a check function?

Answer: So failure messages point to the line that called the helper

t.Helper marks the function as a helper for line reporting.

Which flag finds data races in concurrent code?

Answer: -race

-race builds with the race detector enabled.

What is a data race?Related

Answer: Unsynchronised concurrent access to the same memory where at least one access is a write

That is the precise definition used by the race detector.

From Mutex, Once, atomic and the race detector

What does 100% code coverage prove?Related

Answer: Every line was run by some test

Coverage shows lines executed, not that results were checked correctly.

From Testing your code

Which test name is the most useful?Related

Answer: returns 0 for an empty cart

A good name states the rule and the situation, so a failure tells you exactly what broke.

From Unit testing: checking one piece at a time

Why is "100% coverage" a poor goal on its own?Related

Answer: Tests can run every line without checking the results, so the number says little about quality

Coverage shows what ran, not what was verified. Use it as a signal.

From Testing, quality gates and artefacts

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Packages, testing and generics” before its exam: Benchmarks and fuzz tests, Generics: type parameters and constraints.
  2. Module 5 exam: Packages, testing and generics · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Concurrency: goroutines, channels and context. 46 topics left in the course before the final exam.

← Previous: Dependencies with Go modules