Testing and Benchmarking
40 minGo has a built-in `testing` package that provides comprehensive testing capabilities without external dependencies. Tests are written in files ending with `_test.go` and are executed with `go test`. The testing package includes unit testing, benchmarking, and example testing. Understanding Go's testing package is essential for writing reliable code.
Test functions are named `TestXxx` and take `*testing.T` as a parameter. Test functions use methods like `t.Error()`, `t.Fatal()`, and `t.Log()` to report failures and log information. Tests are run with `go test` and can be filtered by name. Understanding test functions enables you to write unit tests for your code.
Benchmark functions are named `BenchmarkXxx` and take `*testing.B` as a parameter. Benchmarks measure performance by running code multiple times. Use `go test -bench=.` to run benchmarks. Benchmarks help identify performance bottlenecks. Understanding benchmarks enables you to optimize your code.
Table-driven tests are a common pattern in Go, where test cases are defined in a slice and iterated over. This pattern reduces code duplication and makes it easy to add new test cases. Table-driven tests are idiomatic Go. Understanding table-driven tests helps you write comprehensive, maintainable tests.
The `testing` package also supports subtests (using `t.Run()`), which allow grouping related tests and running them selectively. Subtests enable better test organization and parallel execution. Understanding subtests helps you write well-organized test suites.
Best practices include writing tests for all public functions, using table-driven tests, testing edge cases and error conditions, keeping tests simple and focused, and running tests frequently. Tests should be fast, independent, and repeatable. Understanding testing enables you to write reliable, maintainable Go code.
Key Concepts
- Go has a built-in testing package.
- Test functions are named TestXxx and take *testing.T.
- Benchmark functions are named BenchmarkXxx and take *testing.B.
- Table-driven tests are a common, idiomatic pattern.
- Subtests enable better test organization.
Learning Objectives
Master
- Writing unit tests with the testing package
- Creating benchmarks to measure performance
- Using table-driven tests for comprehensive coverage
- Organizing tests with subtests
Develop
- Testing strategy thinking
- Understanding test-driven development
- Writing maintainable, reliable test suites
Tips
- Write tests for all public functions.
- Use table-driven tests to reduce duplication.
- Test edge cases and error conditions.
- Run tests frequently during development.
Common Pitfalls
- Not writing tests, leading to unreliable code.
- Writing tests that are too complex or test implementation details.
- Not testing error conditions and edge cases.
- Writing tests that depend on external state or order.
Summary
- Go's testing package provides comprehensive testing capabilities.
- Test functions use *testing.T; benchmark functions use *testing.B.
- Table-driven tests are idiomatic and reduce duplication.
- Subtests enable better test organization.
- Understanding testing enables reliable, maintainable code.
Exercise
Create comprehensive tests and benchmarks for Go functions.
package main
import (
"testing"
"time"
)
// Functions to test
func add(a, b int) int {
return a + b
}
func multiply(a, b int) int {
return a * b
}
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
func slowFunction() int {
time.Sleep(time.Millisecond * 10)
return 42
}
// Test functions
func TestAdd(t *testing.T) {
result := add(2, 3)
expected := 5
if result != expected {
t.Errorf("add(2, 3) = %d; expected %d", result, expected)
}
}
func TestMultiply(t *testing.T) {
result := multiply(4, 5)
expected := 20
if result != expected {
t.Errorf("multiply(4, 5) = %d; expected %d", result, expected)
}
}
// Table-driven test
func TestAddTable(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 1, 2, 3},
{"negative numbers", -1, -2, -3},
{"zero", 0, 5, 5},
{"large numbers", 1000, 2000, 3000},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := add(test.a, test.b)
if result != test.expected {
t.Errorf("add(%d, %d) = %d; expected %d",
test.a, test.b, result, test.expected)
}
})
}
}
// Benchmark functions
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
add(1, 2)
}
}
func BenchmarkMultiply(b *testing.B) {
for i := 0; i < b.N; i++ {
multiply(3, 4)
}
}
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
fibonacci(20)
}
}
func BenchmarkSlowFunction(b *testing.B) {
for i := 0; i < b.N; i++ {
slowFunction()
}
}
// Example function (documentation)
func ExampleAdd() {
result := add(3, 4)
fmt.Println(result)
// Output: 7
}
// Test with subtests
func TestMathOperations(t *testing.T) {
t.Run("addition", func(t *testing.T) {
if add(1, 1) != 2 {
t.Error("1 + 1 should equal 2")
}
})
t.Run("multiplication", func(t *testing.T) {
if multiply(2, 3) != 6 {
t.Error("2 * 3 should equal 6")
}
})
}
// Test helper function
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestWithHelper(t *testing.T) {
assertEqual(t, add(5, 5), 10)
assertEqual(t, multiply(3, 4), 12)
}
// Main function for demonstration
func main() {
// This would normally be in a separate test file
fmt.Println("Running tests...")
// Example usage
fmt.Printf("add(5, 3) = %d
", add(5, 3))
fmt.Printf("multiply(4, 6) = %d
", multiply(4, 6))
fmt.Printf("fibonacci(10) = %d
", fibonacci(10))
}