Testing
Go's built-in testing package vs pytest — table-driven vs parametrize
Introduction
In this lesson, you'll learn about testing in Go. Coming from Python, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In Python, you're familiar with go's built-in testing package vs pytest — table-driven vs parametrize.
Go has its own approach to go's built-in testing package vs pytest — table-driven vs parametrize, 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
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 (= pytest.parametrize)
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b float64
want float64
wantErr bool
}{
{"normal 6/3", 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") }
return
}
if err != nil { t.Fatalf("unexpected: %v", err) }
if got != tc.want {
t.Errorf("got %v; want %v", got, tc.want)
}
})
}
}
// TestMain for setup/teardown (= pytest fixtures)
func TestMain(m *testing.M) {
setupDB()
code := m.Run()
teardownDB()
os.Exit(code)
}
// Run: go test ./...
// Verbose: go test -v ./...
// Specific: go test -run TestAdd ./...
// Coverage: go test -cover ./...Comparing to Python
Here's how you might have written similar code in Python:
# test_math.py
import pytest
from math_module import add, divide
def test_add():
assert add(1, 2) == 3
assert add(-1, 1) == 0
# Parametrized tests
@pytest.mark.parametrize("a,b,expected", [
(2, 1, 2),
(6, 3, 2),
(0, 5, 0),
])
def test_divide(a, b, expected):
assert divide(a, b) == expected
# Test exceptions
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)
# Fixtures
@pytest.fixture
def db():
conn = connect_db()
yield conn
conn.close()
def test_query(db):
result = db.query("SELECT 1")
assert result == 1
# Run: pytest
# Run verbose: pytest -v
# Run specific: pytest test_math.py::test_addYou may be used to different syntax or behavior.
Go test files end in _test.go; test functions are TestXxx(t *testing.T)
You may be used to different syntax or behavior.
Table-driven tests (slice of structs + range) replaces pytest.parametrize
You may be used to different syntax or behavior.
TestMain(m *testing.M) replaces pytest session-level fixtures
You may be used to different syntax or behavior.
t.Error continues the test; t.Fatal stops — unlike pytest which always stops on assert
You may be used to different syntax or behavior.
No test runner needed — 'go test' is part of the Go toolchain
Step-by-Step Breakdown
1. Test File Conventions
Test files end in _test.go. They can be in the same package (white-box) or package_test (black-box). No framework needed.
# test_math.py — any file starting with test_// math_test.go — must end in _test.go
package math // or: package math_test
import "testing"
func TestAdd(t *testing.T) { ... }2. Table-Driven Tests
Go's idiom for parametrized tests. Define test cases in a struct slice, loop with t.Run() for sub-tests.
@pytest.mark.parametrize("a,b,c", [(1,2,3),(4,5,9)])
def test_add(a, b, c): assert add(a,b) == ctests := []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 failure and continues; t.Fatal stops the test. Use Fatal when subsequent checks would be meaningless.
assert result is not None # pytest stops here
assert result.name == "Alice"if result == nil { t.Fatal("result is nil") } // stop
if result.Name != "Alice" { t.Errorf(...) } // continue4. Benchmarks
Benchmark functions measure performance. The testing framework sets b.N to get statistically significant results.
# No pytest built-in; use pytest-benchmark pluginfunc BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
// go test -bench=BenchmarkAdd -benchmemCommon Mistakes
When coming from Python, developers often make these mistakes:
- Go test files end in _test.go; test functions are TestXxx(t *testing.T)
- Table-driven tests (slice of structs + range) replaces pytest.parametrize
- TestMain(m *testing.M) replaces pytest session-level fixtures
Key Takeaways
- _test.go files; TestXxx(t *testing.T) — no framework, no assertions library needed
- Table-driven tests: struct slice + t.Run() = pytest.parametrize
- t.Error continues; t.Fatal stops — choose based on whether subsequent checks are meaningful
- go test -cover for coverage; -bench for benchmarks; -run=Regexp for filtering