Writing Unit Tests in Go
Go provides a built-in testing
package that makes it easy to write and run unit tests. This guide covers the fundamentals of unit testing in Go.
Basic Test Structure
Writing Your First Test
// math.go
package math
func Add(a, b int) int {
return a + b
}
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
Test File Naming
- Test files should end with
_test.go
- Test functions should start with
Test
- Test function names should be descriptive
- Place test files in the same package as the code being tested
Test Functions
Test Function Structure
func TestFunctionName(t *testing.T) {
// Arrange - Set up test data
input1 := "test"
input2 := 42
expected := "expected result"
// Act - Call the function being tested
result := FunctionBeingTested(input1, input2)
// Assert - Check the results
if result != expected {
t.Errorf("FunctionBeingTested(%q, %d) = %q; want %q",
input1, input2, result, expected)
}
}
Testing Multiple Cases
func TestMultipleCases(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{"empty string", "", false},
{"valid string", "hello", true},
{"special chars", "!@#", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ValidateString(tt.input)
if result != tt.expected {
t.Errorf("ValidateString(%q) = %v; want %v",
tt.input, result, tt.expected)
}
})
}
}
Test Helpers
Creating Helper Functions
func setupTestCase(t *testing.T) func() {
t.Helper() // Marks this as a helper function
// Setup code
db := OpenTestDB()
// Return cleanup function
return func() {
db.Close()
}
}
func TestWithHelper(t *testing.T) {
cleanup := setupTestCase(t)
defer cleanup()
// Test code here
}
Assert Functions
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf("got %v; want %v", got, want)
}
}
func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
Testing Errors
Error Testing
func TestErrorHandling(t *testing.T) {
// Test for expected error
_, err := ParseConfig("invalid")
if err == nil {
t.Error("expected error, got nil")
}
// Test for specific error
if !errors.Is(err, ErrInvalidConfig) {
t.Errorf("got error %v; want %v", err, ErrInvalidConfig)
}
// Test error message
want := "invalid configuration"
if got := err.Error(); got != want {
t.Errorf("error message = %q; want %q", got, want)
}
}
Testing Panics
func TestPanic(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("expected panic, but function did not panic")
}
}()
// Function that should panic
DangerousFunction()
}
Setup and Teardown
Test Main
func TestMain(m *testing.M) {
// Setup code
setupTestEnvironment()
// Run tests
code := m.Run()
// Cleanup code
cleanupTestEnvironment()
// Exit with test result code
os.Exit(code)
}
Individual Test Setup
type TestSuite struct {
db *sql.DB
server *httptest.Server
}
func setupTest(t *testing.T) *TestSuite {
t.Helper()
suite := &TestSuite{
db: setupTestDB(t),
server: setupTestServer(t),
}
t.Cleanup(func() {
suite.db.Close()
suite.server.Close()
})
return suite
}
Best Practices
Keep Tests Focused
// Good - Single focused test func TestUserValidation(t *testing.T) { user := User{Name: "", Age: -1} err := user.Validate() if err == nil { t.Error("expected validation error") } }
Use Clear Error Messages
// Good - Clear error message if got != want { t.Errorf("ProcessString(%q) = %q; want %q", input, got, want) } // Bad - Unclear error message if got != want { t.Error("wrong result") }
Test Edge Cases
func TestDivide(t *testing.T) { tests := []struct { name string a, b float64 want float64 wantErr bool }{ {"valid division", 10, 2, 5, false}, {"zero division", 1, 0, 0, true}, {"negative numbers", -10, -2, 5, false}, {"very large numbers", 1e308, 2, 5e307, false}, } // ... test implementation }
Use Subtests for Organization
func TestUser(t *testing.T) { t.Run("validation", func(t *testing.T) { // Test user validation }) t.Run("serialization", func(t *testing.T) { // Test user serialization }) }
Running Tests
# Run all tests in current package
go test
# Run tests with coverage
go test -cover
# Run specific test
go test -run TestFunctionName
# Run tests verbosely
go test -v
# Run tests in all subdirectories
go test ./...
Next Steps
- Learn about Table Tests
- Explore Benchmarking
- Study Test Coverage