Unit Testing in Go
Unit testing is the process of testing individual pieces of code, like functions or methods, to make sure they work as expected. In Go, unit tests are written using the built-in testing package, which makes it straightforward to create and run tests.
Basic Unit Test
Let’s start with a simple function and write a test for it. Suppose we have a function that adds two numbers:
package math
func Add(a, b int) int {
return a + b
}To test this function, create a file ending with _test.go in the same package:
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}Run the test with go test in your terminal. If everything is correct, you’ll see something like PASS.
Table Driven Tests
For testing multiple cases of the same function, use table driven tests. This approach makes your tests more organized and easier to extend.
package math
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", 5, -3, 2},
}
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)
}
})
}
}This method lets you test various scenarios without repeating code.
Testing Private Functions
Sometimes you need to test functions that aren’t exported (start with lowercase). You can do this by putting the test in the same package. For example:
package utils
func isEven(n int) bool {
return n%2 == 0
}
func ProcessNumbers(nums []int) []int {
var result []int
for _, num := range nums {
if isEven(num) {
result = append(result, num*2)
}
}
return result
}package utils
import "testing"
func TestIsEven(t *testing.T) {
tests := []struct {
input int
expected bool
}{
{2, true},
{3, false},
{0, true},
{-2, true},
}
for _, tt := range tests {
result := isEven(tt.input)
if result != tt.expected {
t.Errorf("isEven(%d) = %v; want %v", tt.input, result, tt.expected)
}
}
}
func TestProcessNumbers(t *testing.T) {
input := []int{1, 2, 3, 4, 5, 6}
expected := []int{4, 8, 12}
result := ProcessNumbers(input)
if len(result) != len(expected) {
t.Fatalf("ProcessNumbers() length = %d; want %d", len(result), len(expected))
}
for i, v := range result {
if v != expected[i] {
t.Errorf("ProcessNumbers()[%d] = %d; want %d", i, v, expected[i])
}
}
}Test Helpers
You can create helper functions to reduce code duplication in your tests. Helpers should be named with a prefix like assert or require.
package math
import "testing"
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestMultiply(t *testing.T) {
assertEqual(t, Multiply(2, 3), 6)
assertEqual(t, Multiply(0, 5), 0)
assertEqual(t, Multiply(-2, 3), -6)
}The t.Helper() call makes the test failure point to the calling line instead of the helper function.
Testing with Setup and Teardown
For tests that need setup or cleanup, use TestMain or setup functions:
package database
import (
"database/sql"
"testing"
)
var testDB *sql.DB
func TestMain(m *testing.M) {
// Setup
var err error
testDB, err = sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
// Create tables
_, err = testDB.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
if err != nil {
panic(err)
}
// Run tests
code := m.Run()
// Teardown
testDB.Close()
// Exit
os.Exit(code)
}
func TestInsertUser(t *testing.T) {
// Test logic using testDB
}Mocking Dependencies
When testing functions that depend on external services, use interfaces to make mocking easier:
package user
type Database interface {
SaveUser(name string) error
}
type UserService struct {
db Database
}
func (s *UserService) CreateUser(name string) error {
return s.db.SaveUser(name)
}package user
import "testing"
type mockDatabase struct {
savedUsers []string
}
func (m *mockDatabase) SaveUser(name string) error {
m.savedUsers = append(m.savedUsers, name)
return nil
}
func TestCreateUser(t *testing.T) {
mockDB := &mockDatabase{}
service := &UserService{db: mockDB}
err := service.CreateUser("John")
if err != nil {
t.Errorf("CreateUser failed: %v", err)
}
if len(mockDB.savedUsers) != 1 || mockDB.savedUsers[0] != "John" {
t.Error("User was not saved correctly")
}
}Running Tests
go test: Run all tests in current packagego test -v: Verbose outputgo test -run TestName: Run specific testgo test -cover: Show coveragego test -coverprofile=coverage.out: Generate coverage profile
Best Practices
- Name your tests clearly: Use descriptive names that explain what they’re testing
- Test one thing at a time: Each test should focus on a single behavior
- Use table driven tests for multiple similar cases
- Test edge cases: Don’t just test happy paths
- Keep tests fast: Slow tests discourage running them
- Use test helpers to reduce duplication
- Test error conditions: Make sure your code handles errors properly
For more information on testing in Go, check out the official testing documentation.
If you’re new to Go basics, you might want to read about Go functions first.