Skip to main content
โšก Calmops

Mocking and Test Doubles in Go

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