Small Rules, Big Difference
Good tests follow a few habits:
1. Clear names — TestGreetEmptyName says what broke without opening the file.
2. One behavior per test — small tests point straight at the problem.
3. Tables for many cases — one loop instead of copied tests.
4. Messages with got and want — a failure should explain itself.
5. t.Error to see everything, t.Fatal when continuing is pointless.
6. Run tests often — after every small change, not once at the end.
7. Tests live next to the code — same folder, _test.go suffix.
Failure Messages That Help
A failing test is read in a moment of confusion — the message must carry the whole story: which call, what came back, what was expected.
The Go convention is Func(input) = got; want want. With %q strings are quoted, so missing spaces or punctuation are visible right away.
A message with the whole story
Greet returns the wrong greeting. The failure message shows the call, the got and the want — the bug (wrong word, missing comma and exclamation mark) is visible in the output alone, without opening the code.
Output: Go test file and captured go test output
package greet
import "testing"
// Greet has a bug: wrong word, missing comma and "!".
func Greet(name string) string {
return "Hi " + name
}
func TestGreet(t *testing.T) {
got := Greet("Ana")
want := "Hello, Ana!"
if got != want {
t.Errorf("Greet(%q) = %q; want %q", "Ana", got, want)
}
}
One Behavior per Test
A test that checks five things has five reasons to fail — and its name can only describe one of them. Splitting by behavior keeps every test small and its name honest.
Here the normal greeting and the empty-name fallback are two behaviors, so they get two tests. When one fails, its name already tells you which behavior broke.
Two behaviors, two tests
TestGreetName checks the normal case, TestGreetEmpty the fallback. The verbose output lists them separately — a failure would point at exactly one behavior.
Output: Go test file and captured go test output
package greet
import "testing"
func Greet(name string) string {
if name == "" {
return "Hello, stranger!"
}
return "Hello, " + name + "!"
}
func TestGreetName(t *testing.T) {
got := Greet("Ana")
if got != "Hello, Ana!" {
t.Errorf("Greet(%q) = %q; want %q", "Ana", got, "Hello, Ana!")
}
}
func TestGreetEmpty(t *testing.T) {
got := Greet("")
if got != "Hello, stranger!" {
t.Errorf("Greet(%q) = %q; want %q", "", got, "Hello, stranger!")
}
}
How Much Is Tested: Coverage
go test -cover measures which part of your code the tests actually execute, as a percentage of statements.
The number is a compass, not a goal: a low value shows untested paths — here the test only walks the mild branch, so the freezing and hot branches are never checked. 100% coverage does not prove the code is correct, but the uncovered lines are guaranteed unchecked.
Measure the coverage
Describe has three branches, the test exercises one. The coverage number reveals it — adding tests for the cold and hot branches would drive it up.
Output: Go test file and captured go test output
package temp
import "testing"
func Describe(celsius int) string {
if celsius < 0 {
return "freezing"
}
if celsius > 30 {
return "hot"
}
return "mild"
}
func TestDescribeMild(t *testing.T) {
got := Describe(20)
if got != "mild" {
t.Errorf("Describe(20) = %q; want %q", got, "mild")
}
}