Intermediate · Packages, testing and generics · topic 23 of 46 · 40 min
Testing with go test
Explanation
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
// gst.go
package gst
func WithGST(paise int64) int64 {
return paise + paise*18/100
}// 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)
}
}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- 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.
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)
}
})
}
}Coverage and the race detector
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)Useful helpers
| Helper | Use |
|---|---|
| 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/ folder | Files for tests; Go tools ignore it when building |
| net/http/httptest | Test HTTP handlers without a real server (module 8) |
Example functions
func ExampleWithGST() {
fmt.Println(WithGST(10000))
// Output: 11800
}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 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.
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)
}
})
}
}Practice (0/4 done)
Which is a valid test function that go test will run?
What is the difference between t.Errorf and t.Fatalf?
Which file name will go test pick up?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Benchmarks and fuzz testsPackages, testing and generics · same module
- Packages, visibility and project layoutPackages, testing and generics · same module
- Mutex, Once, atomic and the race detectorConcurrency: goroutines, channels and context
- Testing your codeJavaScript Programming (language in depth) · Functional patterns and testing
- Unit testing: checking one piece at a timeBackend Development · Testing and API documentation
- Testing, quality gates and artefactsDevOps: Git, Docker, Kubernetes and CI/CD · CI/CD pipelines
Questions people ask about this
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.
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.
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.
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
- 2 more topics in “Packages, testing and generics” before its exam: Benchmarks and fuzz tests, Generics: type parameters and constraints.
- Module 5 exam: Packages, testing and generics · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Concurrency: goroutines, channels and context. 46 topics left in the course before the final exam.