Testing
Go's built-in testing package and table-driven tests
Introduction
In this lesson, you'll learn about testing in Go. Coming from JavaScript, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In JavaScript, you're familiar with go's built-in testing package and table-driven tests.
Go has its own approach to go's built-in testing package and table-driven tests, which we'll explore step by step.
The Go Way
Let's see how Go handles this concept. Here's a typical example:
// math_test.go (same package, _test.go suffix)
package math
import "testing"
func TestAdd(t *testing.T) {
if got := Add(1, 2); got != 3 {
t.Errorf("Add(1,2) = %d; want 3", got)
}
}
// Table-driven test (idiomatic Go)
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b float64
want float64
wantErr bool
}{
{"normal", 6, 3, 2, false},
{"zero dividend", 0, 5, 0, false},
{"divide by zero", 1, 0, 0, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := Divide(tc.a, tc.b)
if tc.wantErr {
if err == nil { t.Error("expected error, got nil") }
return
}
if err != nil { t.Fatalf("unexpected error: %v", err) }
if got != tc.want { t.Errorf("got %v; want %v", got, tc.want) }
})
}
}
// Benchmark
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
// Run: go test ./...
// Run with coverage: go test -cover ./...
// Run benchmark: go test -bench=. ./...Comparing to JavaScript
Here's how you might have written similar code in JavaScript:
// math.test.js
import { add, divide } from "./math";
describe("math", () => {
test("add returns sum", () => {
expect(add(1, 2)).toBe(3);
expect(add(-1, 1)).toBe(0);
});
test.each([
[2, 1, 2],
[6, 3, 2],
[0, 5, 0],
])("divide(%i, %i) = %i", (a, b, expected) => {
expect(divide(a, b)).toBe(expected);
});
test("divide by zero throws", () => {
expect(() => divide(1, 0)).toThrow("division by zero");
});
});
// Run: npx jestYou may be used to different syntax or behavior.
Test files end in _test.go; test functions are TestXxx(t *testing.T)
You may be used to different syntax or behavior.
No test runner framework needed — 'go test' is built into the toolchain
You may be used to different syntax or behavior.
t.Run() creates sub-tests; table-driven tests are idiomatic Go
You may be used to different syntax or behavior.
t.Error/Errorf continues the test; t.Fatal/Fatalf stops immediately
You may be used to different syntax or behavior.
Benchmarks are BenchmarkXxx(b *testing.B); run with -bench flag
Step-by-Step Breakdown
1. Test File Convention
Go test files end in _test.go and live in the same package (or package_test for black-box testing). No separate test runner needed.
// math.test.js — external file, Jest runner// math_test.go — same directory as math.go
package math // or: package math_test for black-box
import "testing"2. Table-Driven Tests
Table-driven tests are the Go idiom for parametrized tests. Define test cases in a slice of structs, then loop with t.Run().
test.each([[1,2,3],[4,5,9]])("add",([a,b,c])=>expect(add(a,b)).toBe(c))tests := []struct{ a,b,want int }{
{1,2,3}, {4,5,9},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%d+%d",tc.a,tc.b), func(t *testing.T){
if got := Add(tc.a,tc.b); got != tc.want {
t.Errorf("got %d; want %d", got, tc.want)
}
})
}3. Error vs Fatal
t.Error marks a test failed but continues. t.Fatal marks failed and stops. Use Fatal when subsequent assertions would be meaningless after failure.
expect(result).toBe(expected); // Jest stops on first failureresult, err := DoThing()
if err != nil { t.Fatalf("DoThing failed: %v", err) } // stop here
if result != expected { t.Errorf("got %v; want %v", result, expected) }4. Benchmarks
Benchmark functions let you measure performance. The testing framework adjusts b.N to get stable results.
// No built-in benchmark in Jest; use console.time or benchmark.jsfunc BenchmarkSort(b *testing.B) {
data := generateData()
b.ResetTimer()
for i := 0; i < b.N; i++ {
sort.Ints(data)
}
}
// go test -bench=BenchmarkSort -benchmemCommon Mistakes
When coming from JavaScript, developers often make these mistakes:
- Test files end in _test.go; test functions are TestXxx(t *testing.T)
- No test runner framework needed — 'go test' is built into the toolchain
- t.Run() creates sub-tests; table-driven tests are idiomatic Go
Key Takeaways
- Test files end in _test.go; functions are TestXxx(t *testing.T) — no framework needed
- Table-driven tests: slice of structs + range + t.Run() is the Go idiom
- t.Error continues after failure; t.Fatal stops — use Fatal when remaining checks are meaningless
- go test -cover shows coverage; -bench runs benchmarks; -run=TestName filters tests