Test Organization and Coverage
Overview
Well-organized tests and good coverage help maintain code quality over time.
File Organization
mypackage/
├── user.go # Implementation
├── user_test.go # Tests
├── user_internal_test.go # Internal tests
└── testdata/ # Test fixtures
└── users.json
Package Naming
// Same package (white-box testing)
package mypackage
// External package (black-box testing)
package mypackage_testTestdata Directory
func TestParseConfig(t *testing.T) {
data, err := os.ReadFile("testdata/config.json")
if err != nil {
t.Fatal(err)
}
// Use data
}Code Coverage
# Show coverage percentage
go test -cover
# Generate coverage profile
go test -coverprofile=coverage.out
# View in browser
go tool cover -html=coverage.out
# Show coverage by function
go tool cover -func=coverage.outCoverage Modes
# Statement coverage (default)
go test -covermode=set
# Count mode (how many times)
go test -covermode=count
# Atomic (for concurrent tests)
go test -covermode=atomicTest Groups
func TestUser(t *testing.T) {
t.Run("Create", func(t *testing.T) {
// Test creation
})
t.Run("Update", func(t *testing.T) {
// Test update
})
t.Run("Delete", func(t *testing.T) {
// Test deletion
})
}Skipping Tests
func TestIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
// Long test
}go test -short # Skip long testsSummary
| Flag | Purpose |
|---|---|
-cover |
Show coverage % |
-coverprofile=file |
Generate profile |
-short |
Skip long tests |
-run=Pattern |
Run matching tests |
Worked example
White-box vs black-box packages and testdata fixtures side by side.
Save under greet/. Then:
cd greet
go mod init example/greet
go test -v ./...
go test -cover// greet.go
package greet
import "fmt"
func Hello(name string) string {
if name == "" {
name = defaultName()
}
return fmt.Sprintf("Hello, %s!", name)
}
func defaultName() string { return "world" }// greet_internal_test.go — white-box (same package)
package greet
import "testing"
func TestDefaultName(t *testing.T) {
if defaultName() != "world" {
t.Fatalf("got %q", defaultName())
}
}// greet_test.go — black-box (external test package)
package greet_test
import (
"os"
"strings"
"testing"
"example/greet"
)
func TestHelloFromFixture(t *testing.T) {
data, err := os.ReadFile("testdata/names.txt")
if err != nil {
t.Fatal(err)
}
for _, name := range strings.Split(strings.TrimSpace(string(data)), "\n") {
got := greet.Hello(name)
if !strings.Contains(got, name) {
t.Errorf("Hello(%q)=%q", name, got)
}
}
}
func TestHelloEmpty(t *testing.T) {
if greet.Hello("") != "Hello, world!" {
t.Fatal(greet.Hello(""))
}
}testdata/names.txt:
Ada
Grace
Expected output: (go test -v)
=== RUN TestDefaultName
--- PASS: TestDefaultName (0.00s)
=== RUN TestHelloFromFixture
--- PASS: TestHelloFromFixture (0.00s)
=== RUN TestHelloEmpty
--- PASS: TestHelloEmpty (0.00s)
PASS
More examples
Grouped subtests + -short skip for slow suites.
// suite_test.go
package greet_test
import (
"testing"
"example/greet"
)
func TestHelloSuite(t *testing.T) {
t.Run("named", func(t *testing.T) {
if greet.Hello("Lin") != "Hello, Lin!" {
t.Fatal("named")
}
})
t.Run("default", func(t *testing.T) {
if greet.Hello("") != "Hello, world!" {
t.Fatal("default")
}
})
t.Run("slow", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping slow case")
}
// pretend expensive integration work
_ = greet.Hello("slow")
})
}go test -short -v -run TestHelloSuiteRunnable example
Save these files under mathx/. Create testdata/cases.txt as shown. Then:
cd mathx
go mod init example/mathx
go test -v
go test -cover
go test -short -v
go test -coverprofile=coverage.out && go tool cover -func=coverage.outmathx.go:
package mathx
import (
"strconv"
"strings"
)
func Add(a, b int) int { return a + b }
// ParseSum reads lines of "a+b" and returns the sums.
func ParseSum(data string) ([]int, error) {
lines := strings.Split(strings.TrimSpace(data), "\n")
out := make([]int, 0, len(lines))
for _, line := range lines {
if line == "" {
continue
}
parts := strings.Split(line, "+")
if len(parts) != 2 {
return nil, errString("bad line: " + line)
}
a, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return nil, err
}
b, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return nil, err
}
out = append(out, Add(a, b))
}
return out, nil
}
type errString string
func (e errString) Error() string { return string(e) }mathx_test.go (black-box: external test package):
package mathx_test
import (
"os"
"testing"
"example/mathx"
)
func TestAdd(t *testing.T) {
t.Run("positive", func(t *testing.T) {
if got := mathx.Add(2, 3); got != 5 {
t.Fatalf("got %d", got)
}
})
t.Run("negative", func(t *testing.T) {
if got := mathx.Add(-2, -3); got != -5 {
t.Fatalf("got %d", got)
}
})
}
func TestParseSumFromTestdata(t *testing.T) {
data, err := os.ReadFile("testdata/cases.txt")
if err != nil {
t.Fatal(err)
}
sums, err := mathx.ParseSum(string(data))
if err != nil {
t.Fatal(err)
}
want := []int{5, 0, 9}
if len(sums) != len(want) {
t.Fatalf("len=%d want %d", len(sums), len(want))
}
for i := range want {
if sums[i] != want[i] {
t.Errorf("sums[%d]=%d want %d", i, sums[i], want[i])
}
}
}
func TestSlow(t *testing.T) {
if testing.Short() {
t.Skip("skipping slow test in short mode")
}
// Pretend this is an expensive integration-style check.
if mathx.Add(100, 1) != 101 {
t.Fatal("unexpected")
}
}testdata/cases.txt:
2+3
-1+1
4+5
Expected output: (go test -v without -short)
=== RUN TestAdd
=== RUN TestAdd/positive
=== RUN TestAdd/negative
--- PASS: TestAdd (0.00s)
=== RUN TestParseSumFromTestdata
--- PASS: TestParseSumFromTestdata (0.00s)
=== RUN TestSlow
--- PASS: TestSlow (0.00s)
PASS
With go test -short -v, TestSlow reports SKIP.
What to notice: package mathx_test only sees exported API (black-box). testdata/ is the conventional place for fixtures. Subtests (t.Run) organize cases; -short skips long work.
Try next: Generate an HTML coverage report with go tool cover -html=coverage.out; add an internal white-box test file with package mathx that exercises an unexported helper.