Unit Testing in Go
Testing is a first-class citizen in Go. The language includes a built-in testing package and encourages test-driven development. This guide covers everything you need to know about writing effective unit tests in Go.
Getting Started with Testing
Basic Test Structure
package main
import (
"testing"
)
// Function to test
func Add(a, b int) int {
return a + b
}
// Test function
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
func TestAddNegative(t *testing.T) {
result := Add(-2, -3)
expected := -5
if result != expected {
t.Errorf("Add(-2, -3) = %d; want %d", result, expected)
}
}
Running Tests
# Run all tests in current package
go test
# Run with verbose output
go test -v
# Run specific test
go test -run TestAdd
# Run with coverage
go test -cover
# Generate coverage report
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
Test Patterns
Table-Driven Tests
package main
import (
"testing"
)
func TestAddTable(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -2, -3, -5},
{"mixed", 5, -3, 2},
{"zero", 0, 0, 0},
{"with zero", 5, 0, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
})
}
}
Subtests
package main
import (
"testing"
)
func TestMath(t *testing.T) {
t.Run("addition", func(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add failed: got %d, want 5", result)
}
})
t.Run("subtraction", func(t *testing.T) {
result := 5 - 3
if result != 2 {
t.Errorf("Subtraction failed: got %d, want 2", result)
}
})
t.Run("multiplication", func(t *testing.T) {
result := 3 * 4
if result != 12 {
t.Errorf("Multiplication failed: got %d, want 12", result)
}
})
}
Test Helpers and Utilities
Helper Functions
package main
import (
"testing"
)
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
func TestWithHelper(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5)
}
Setup and Teardown
package main
import (
"testing"
)
func TestWithSetup(t *testing.T) {
// Setup
data := []int{1, 2, 3, 4, 5}
// Test
if len(data) != 5 {
t.Errorf("Expected 5 elements, got %d", len(data))
}
// Teardown (if needed)
data = nil
}
func TestWithCleanup(t *testing.T) {
// Setup
resource := "test resource"
// Register cleanup
t.Cleanup(func() {
// Cleanup code
resource = ""
})
// Test
if resource != "test resource" {
t.Errorf("Resource not initialized")
}
}
Testing Different Scenarios
Testing Errors
package main
import (
"errors"
"testing"
)
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func TestDivideError(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("Expected error for division by zero")
}
if err.Error() != "division by zero" {
t.Errorf("Wrong error message: %v", err)
}
}
func TestDivideSuccess(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if result != 5 {
t.Errorf("Expected 5, got %d", result)
}
}
Testing Interfaces
package main
import (
"testing"
)
type Reader interface {
Read() string
}
type MockReader struct {
data string
}
func (m *MockReader) Read() string {
return m.data
}
func ProcessReader(r Reader) string {
return "Processed: " + r.Read()
}
func TestInterface(t *testing.T) {
mock := &MockReader{data: "test"}
result := ProcessReader(mock)
expected := "Processed: test"
if result != expected {
t.Errorf("Expected %q, got %q", expected, result)
}
}
Testing Concurrency
package main
import (
"sync"
"testing"
"time"
)
func TestConcurrency(t *testing.T) {
var wg sync.WaitGroup
results := make([]int, 0)
var mu sync.Mutex
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
mu.Lock()
results = append(results, n)
mu.Unlock()
}(i)
}
wg.Wait()
if len(results) != 10 {
t.Errorf("Expected 10 results, got %d", len(results))
}
}
func TestTimeout(t *testing.T) {
done := make(chan bool)
go func() {
time.Sleep(100 * time.Millisecond)
done <- true
}()
select {
case <-done:
// Success
case <-time.After(1 * time.Second):
t.Error("Test timed out")
}
}
Practical Testing Examples
Testing String Functions
package main
import (
"strings"
"testing"
)
func TestStringFunctions(t *testing.T) {
tests := []struct {
name string
input string
contains string
expected bool
}{
{"contains", "hello world", "world", true},
{"not contains", "hello world", "xyz", false},
{"empty", "", "test", false},
{"case sensitive", "Hello", "hello", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := strings.Contains(tt.input, tt.contains)
if result != tt.expected {
t.Errorf("Contains(%q, %q) = %v; want %v",
tt.input, tt.contains, result, tt.expected)
}
})
}
}
Testing Slices
package main
import (
"testing"
)
func Contains(slice []int, value int) bool {
for _, v := range slice {
if v == value {
return true
}
}
return false
}
func TestSliceOperations(t *testing.T) {
slice := []int{1, 2, 3, 4, 5}
tests := []struct {
name string
value int
expected bool
}{
{"found", 3, true},
{"not found", 10, false},
{"first", 1, true},
{"last", 5, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Contains(slice, tt.value)
if result != tt.expected {
t.Errorf("Contains(%v, %d) = %v; want %v",
slice, tt.value, result, tt.expected)
}
})
}
}
Testing Maps
package main
import (
"testing"
)
func TestMapOperations(t *testing.T) {
m := map[string]int{
"apple": 5,
"banana": 3,
"cherry": 8,
}
tests := []struct {
name string
key string
expected int
exists bool
}{
{"exists", "apple", 5, true},
{"not exists", "date", 0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
value, ok := m[tt.key]
if ok != tt.exists {
t.Errorf("Key %q exists = %v; want %v", tt.key, ok, tt.exists)
}
if ok && value != tt.expected {
t.Errorf("m[%q] = %d; want %d", tt.key, value, tt.expected)
}
})
}
}
Best Practices
โ Good Practices
// Use table-driven tests
func TestFunction(t *testing.T) {
tests := []struct {
name string
input interface{}
expected interface{}
}{
// Test cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test logic
})
}
}
// Use t.Helper() for helper functions
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
// Test error cases
func TestError(t *testing.T) {
_, err := functionThatErrors()
if err == nil {
t.Error("Expected error")
}
}
// Use subtests for organization
t.Run("subtest", func(t *testing.T) {
// Subtest logic
})
โ Anti-Patterns
// Don't use generic test names
func TestFunction(t *testing.T) {
// Hard to identify which case failed
}
// Don't ignore errors
result, _ := functionThatErrors()
// Should check error
// Don't test implementation details
// Test behavior, not internal state
// Don't create interdependent tests
// Each test should be independent
Resources
Summary
Go’s testing package provides powerful tools for writing effective tests:
- Use table-driven tests for multiple scenarios
- Organize tests with subtests
- Use helper functions for common assertions
- Test both success and error cases
- Keep tests independent and focused
- Aim for good coverage but prioritize meaningful tests
With these practices, you can build reliable, well-tested Go applications.
Comments