Mocking and Test Doubles in Go
Mocking and test doubles are essential techniques for writing effective unit tests. They allow you to isolate code under test and verify behavior without external dependencies. This guide covers practical mocking techniques in Go.
Understanding Test Doubles
Types of Test Doubles
package main
// Original interface
type PaymentGateway interface {
Charge(amount float64) (transactionID string, err error)
}
// Stub - returns predefined responses
type StubPaymentGateway struct {
TransactionID string
}
func (s *StubPaymentGateway) Charge(amount float64) (string, error) {
return s.TransactionID, nil
}
// Fake - working implementation for testing
type FakePaymentGateway struct {
Transactions map[string]float64
}
func (f *FakePaymentGateway) Charge(amount float64) (string, error) {
id := "fake_" + fmt.Sprintf("%d", len(f.Transactions))
f.Transactions[id] = amount
return id, nil
}
// Mock - verifies interactions
type MockPaymentGateway struct {
ChargeCalled bool
ChargeAmount float64
}
func (m *MockPaymentGateway) Charge(amount float64) (string, error) {
m.ChargeCalled = true
m.ChargeAmount = amount
return "mock_123", nil
}
// Spy - records calls while delegating to real implementation
type SpyPaymentGateway struct {
Real PaymentGateway
CallCount int
LastAmount float64
}
func (s *SpyPaymentGateway) Charge(amount float64) (string, error) {
s.CallCount++
s.LastAmount = amount
return s.Real.Charge(amount)
}
Creating Mocks
Manual Mock Implementation
package main
import (
"testing"
)
type UserRepository interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type User struct {
ID string
Name string
}
type MockUserRepository struct {
GetUserCalled bool
GetUserID string
GetUserResult *User
GetUserError error
SaveUserCalled bool
SaveUserUser *User
SaveUserError error
}
func (m *MockUserRepository) GetUser(id string) (*User, error) {
m.GetUserCalled = true
m.GetUserID = id
return m.GetUserResult, m.GetUserError
}
func (m *MockUserRepository) SaveUser(user *User) error {
m.SaveUserCalled = true
m.SaveUserUser = user
return m.SaveUserError
}
// Service using the repository
type UserService struct {
repo UserRepository
}
func (s *UserService) UpdateUserName(id, newName string) error {
user, err := s.repo.GetUser(id)
if err != nil {
return err
}
user.Name = newName
return s.repo.SaveUser(user)
}
// Test using mock
func TestUpdateUserName(t *testing.T) {
mock := &MockUserRepository{
GetUserResult: &User{ID: "1", Name: "Old Name"},
}
service := &UserService{repo: mock}
err := service.UpdateUserName("1", "New Name")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !mock.GetUserCalled {
t.Error("GetUser was not called")
}
if !mock.SaveUserCalled {
t.Error("SaveUser was not called")
}
if mock.SaveUserUser.Name != "New Name" {
t.Errorf("Expected name 'New Name', got '%s'", mock.SaveUserUser.Name)
}
}
Using testify/mock
package main
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockDatabase struct {
mock.Mock
}
func (m *MockDatabase) Query(sql string) ([]string, error) {
args := m.Called(sql)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]string), args.Error(1)
}
func TestDatabaseQuery(t *testing.T) {
mockDB := new(MockDatabase)
// Set expectations
mockDB.On("Query", "SELECT * FROM users").Return([]string{"user1", "user2"}, nil)
// Call the mock
result, err := mockDB.Query("SELECT * FROM users")
// Verify
assert.NoError(t, err)
assert.Equal(t, []string{"user1", "user2"}, result)
mockDB.AssertExpectations(t)
}
Practical Mocking Examples
Mocking HTTP Clients
package main
import (
"io"
"net/http"
"strings"
"testing"
)
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type APIClient struct {
client HTTPClient
}
func (c *APIClient) GetUser(id string) (string, error) {
req, _ := http.NewRequest("GET", "https://api.example.com/users/"+id, nil)
resp, err := c.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return string(body), nil
}
type MockHTTPClient struct {
DoFunc func(req *http.Request) (*http.Response, error)
}
func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
return m.DoFunc(req)
}
func TestGetUser(t *testing.T) {
mockClient := &MockHTTPClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`{"id":"1","name":"Alice"}`)),
}, nil
},
}
client := &APIClient{client: mockClient}
result, err := client.GetUser("1")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !strings.Contains(result, "Alice") {
t.Errorf("Expected Alice in response, got: %s", result)
}
}
Mocking Database Operations
package main
import (
"testing"
)
type Database interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
DeleteUser(id string) error
}
type FakeDatabase struct {
users map[string]*User
}
func NewFakeDatabase() *FakeDatabase {
return &FakeDatabase{
users: make(map[string]*User),
}
}
func (f *FakeDatabase) GetUser(id string) (*User, error) {
user, ok := f.users[id]
if !ok {
return nil, nil
}
return user, nil
}
func (f *FakeDatabase) SaveUser(user *User) error {
f.users[user.ID] = user
return nil
}
func (f *FakeDatabase) DeleteUser(id string) error {
delete(f.users, id)
return nil
}
func TestUserOperations(t *testing.T) {
db := NewFakeDatabase()
// Test save
user := &User{ID: "1", Name: "Alice"}
db.SaveUser(user)
// Test get
retrieved, _ := db.GetUser("1")
if retrieved.Name != "Alice" {
t.Errorf("Expected Alice, got %s", retrieved.Name)
}
// Test delete
db.DeleteUser("1")
retrieved, _ = db.GetUser("1")
if retrieved != nil {
t.Error("Expected user to be deleted")
}
}
Mocking Time
package main
import (
"testing"
"time"
)
type Clock interface {
Now() time.Time
}
type RealClock struct{}
func (r *RealClock) Now() time.Time {
return time.Now()
}
type MockClock struct {
CurrentTime time.Time
}
func (m *MockClock) Now() time.Time {
return m.CurrentTime
}
type Scheduler struct {
clock Clock
}
func (s *Scheduler) IsExpired(deadline time.Time) bool {
return s.clock.Now().After(deadline)
}
func TestSchedulerExpiration(t *testing.T) {
mockClock := &MockClock{
CurrentTime: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC),
}
scheduler := &Scheduler{clock: mockClock}
deadline := time.Date(2025, 1, 1, 11, 0, 0, 0, time.UTC)
if !scheduler.IsExpired(deadline) {
t.Error("Expected deadline to be expired")
}
deadline = time.Date(2025, 1, 1, 13, 0, 0, 0, time.UTC)
if scheduler.IsExpired(deadline) {
t.Error("Expected deadline to not be expired")
}
}
Advanced Mocking Patterns
Spy Pattern
package main
import (
"testing"
)
type SpyLogger struct {
Logs []string
}
func (s *SpyLogger) Log(message string) {
s.Logs = append(s.Logs, message)
}
type Application struct {
logger interface{ Log(string) }
}
func (a *Application) ProcessUser(user *User) {
a.logger.Log("Processing user: " + user.ID)
// Process user
a.logger.Log("User processed: " + user.ID)
}
func TestApplicationLogging(t *testing.T) {
spy := &SpyLogger{}
app := &Application{logger: spy}
user := &User{ID: "1", Name: "Alice"}
app.ProcessUser(user)
if len(spy.Logs) != 2 {
t.Errorf("Expected 2 logs, got %d", len(spy.Logs))
}
if spy.Logs[0] != "Processing user: 1" {
t.Errorf("Unexpected first log: %s", spy.Logs[0])
}
}
Assertion Helper
package main
import (
"testing"
)
type AssertMock struct {
t *testing.T
}
func (a *AssertMock) Called(name string, times int, actual int) {
if actual != times {
a.t.Errorf("%s called %d times, expected %d", name, actual, times)
}
}
func (a *AssertMock) Equal(name string, expected, actual interface{}) {
if expected != actual {
a.t.Errorf("%s: expected %v, got %v", name, expected, actual)
}
}
func TestWithAssertions(t *testing.T) {
assert := &AssertMock{t: t}
mock := &MockUserRepository{
GetUserResult: &User{ID: "1", Name: "Alice"},
}
service := &UserService{repo: mock}
service.UpdateUserName("1", "Bob")
assert.Called("GetUser", 1, 1)
assert.Called("SaveUser", 1, 1)
assert.Equal("SaveUser name", "Bob", mock.SaveUserUser.Name)
}
Best Practices
โ Good Practices
// Use interfaces for mockability
type Repository interface {
GetUser(id string) (*User, error)
}
// Create focused mocks
type MockRepository struct {
GetUserResult *User
GetUserError error
}
// Verify behavior, not implementation
mock.AssertExpectations(t)
// Use table-driven tests with mocks
tests := []struct {
name string
mock *MockRepository
expected interface{}
}{
// Test cases
}
// Keep mocks simple and focused
// One mock per interface
โ Anti-Patterns
// Don't mock everything
// Only mock external dependencies
// Don't create overly complex mocks
// Keep them simple and readable
// Don't test the mock
// Test the code using the mock
// Don't use mocks for simple types
// Use real instances when possible
Resources
Summary
Mocking and test doubles enable:
- Isolation of code under test
- Testing without external dependencies
- Verification of behavior
- Faster, more reliable tests
- Better test organization
Key patterns include stubs, fakes, mocks, and spies. Use them strategically to write effective unit tests.
Comments