1. go
  2. /testing
  3. /mocking

Mocking and Test Doubles in Go

Go's interface system makes it easy to create mocks and test doubles. This guide covers various mocking techniques and best practices.

Interface-Based Mocking

Basic Interface Mocking

// Interface definition
type UserRepository interface {
    GetUser(id string) (*User, error)
    SaveUser(user *User) error
}

// Mock implementation
type MockUserRepository struct {
    users map[string]*User
}

func NewMockUserRepository() *MockUserRepository {
    return &MockUserRepository{
        users: make(map[string]*User),
    }
}

func (m *MockUserRepository) GetUser(id string) (*User, error) {
    user, exists := m.users[id]
    if !exists {
        return nil, fmt.Errorf("user not found: %s", id)
    }
    return user, nil
}

func (m *MockUserRepository) SaveUser(user *User) error {
    m.users[user.ID] = user
    return nil
}

Testing with Mocks

func TestUserService(t *testing.T) {
    // Create mock repository
    mockRepo := NewMockUserRepository()
    
    // Create service with mock
    service := NewUserService(mockRepo)
    
    // Test service behavior
    user := &User{ID: "123", Name: "John"}
    err := service.CreateUser(user)
    if err != nil {
        t.Errorf("CreateUser() error = %v", err)
    }
    
    // Verify mock interaction
    savedUser, err := mockRepo.GetUser("123")
    if err != nil {
        t.Errorf("GetUser() error = %v", err)
    }
    if savedUser.Name != "John" {
        t.Errorf("got name %q, want %q", savedUser.Name, "John")
    }
}

Advanced Mocking Techniques

Recording Method Calls

type MockWithRecording struct {
    calls []string
    data  map[string]interface{}
}

func (m *MockWithRecording) recordCall(method string, args ...interface{}) {
    call := fmt.Sprintf("%s(%v)", method, args)
    m.calls = append(m.calls, call)
}

func (m *MockWithRecording) GetCalls() []string {
    return m.calls
}

func (m *MockWithRecording) Method(arg string) error {
    m.recordCall("Method", arg)
    return nil
}

Configurable Responses

type MockDB struct {
    responses map[string]interface{}
    errors    map[string]error
}

func NewMockDB() *MockDB {
    return &MockDB{
        responses: make(map[string]interface{}),
        errors:    make(map[string]error),
    }
}

func (m *MockDB) SetResponse(key string, value interface{}) {
    m.responses[key] = value
}

func (m *MockDB) SetError(key string, err error) {
    m.errors[key] = err
}

func (m *MockDB) Get(key string) (interface{}, error) {
    if err, exists := m.errors[key]; exists {
        return nil, err
    }
    if value, exists := m.responses[key]; exists {
        return value, nil
    }
    return nil, fmt.Errorf("no response configured for key: %s", key)
}

Using Mocking Libraries

Using testify/mock

import "github.com/stretchr/testify/mock"

type MockService struct {
    mock.Mock
}

func (m *MockService) GetData(id string) (*Data, error) {
    args := m.Called(id)
    return args.Get(0).(*Data), args.Error(1)
}

func TestWithTestify(t *testing.T) {
    mockService := new(MockService)
    
    // Setup expectations
    mockService.On("GetData", "123").Return(&Data{}, nil)
    
    // Use mock
    data, err := mockService.GetData("123")
    
    // Assert expectations
    mockService.AssertExpectations(t)
}

Using gomock

//go:generate mockgen -source=service.go -destination=mock_service.go -package=service

package service

type DataService interface {
    GetData(id string) (*Data, error)
    SaveData(data *Data) error
}

func TestWithGoMock(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()
    
    mockService := NewMockDataService(ctrl)
    
    // Setup expectations
    mockService.EXPECT().
        GetData("123").
        Return(&Data{}, nil)
    
    // Use mock
    data, err := mockService.GetData("123")
}

Best Practices

1. Keep Mocks Simple

// Good - Simple mock
type SimpleStoreMock struct {
    data map[string]string
}

func (s *SimpleStoreMock) Get(key string) string {
    return s.data[key]
}

// Bad - Overly complex mock
type ComplexStoreMock struct {
    data       map[string]string
    accessLog  []string
    errorRates map[string]float64
    // Too many features that aren't needed
}

2. Mock at the Right Level

// Good - Mock at interface boundary
type UserStore interface {
    GetUser(id string) (*User, error)
}

// Bad - Mock implementation details
type UserStoreImpl struct {
    db    *sql.DB
    cache *redis.Client
    // Don't mock these internal details
}

3. Use Table-Driven Tests with Mocks

func TestUserService(t *testing.T) {
    tests := []struct {
        name      string
        mockSetup func(*MockUserStore)
        input     string
        want      *User
        wantErr   bool
    }{
        {
            name: "successful get",
            mockSetup: func(m *MockUserStore) {
                m.On("GetUser", "123").Return(&User{ID: "123"}, nil)
            },
            input: "123",
            want:  &User{ID: "123"},
        },
        {
            name: "not found",
            mockSetup: func(m *MockUserStore) {
                m.On("GetUser", "456").Return(nil, errors.New("not found"))
            },
            input:   "456",
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            mock := NewMockUserStore()
            tt.mockSetup(mock)
            
            service := NewUserService(mock)
            got, err := service.GetUser(tt.input)
            
            if (err != nil) != tt.wantErr {
                t.Errorf("got error = %v, wantErr %v", err, tt.wantErr)
            }
            if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
                t.Errorf("got = %v, want %v", got, tt.want)
            }
        })
    }
}

4. Verify Mock Interactions

type MockWithVerification struct {
    calls map[string]int
}

func (m *MockWithVerification) Method(arg string) {
    if m.calls == nil {
        m.calls = make(map[string]int)
    }
    m.calls[arg]++
}

func (m *MockWithVerification) VerifyCalls(t *testing.T, method string, expected int) {
    t.Helper()
    if actual := m.calls[method]; actual != expected {
        t.Errorf("got %d calls to %s, want %d", actual, method, expected)
    }
}

Common Patterns

Mocking Time

type TimeProvider interface {
    Now() time.Time
}

type MockTime struct {
    current time.Time
}

func (m *MockTime) Now() time.Time {
    return m.current
}

func (m *MockTime) Set(t time.Time) {
    m.current = t
}

func TestWithMockTime(t *testing.T) {
    mock := &MockTime{current: time.Date(2024, 3, 19, 0, 0, 0, 0, time.UTC)}
    service := NewService(mock)
    
    // Test with fixed time
    result := service.Process()
    
    // Advance time
    mock.Set(mock.current.Add(24 * time.Hour))
    
    // Test with new time
    result = service.Process()
}

Mocking HTTP Responses

type MockHTTPClient struct {
    responses map[string]*http.Response
}

func NewMockHTTPClient() *MockHTTPClient {
    return &MockHTTPClient{
        responses: make(map[string]*http.Response),
    }
}

func (m *MockHTTPClient) SetResponse(url string, resp *http.Response) {
    m.responses[url] = resp
}

func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
    if resp, ok := m.responses[req.URL.String()]; ok {
        return resp, nil
    }
    return nil, fmt.Errorf("no response configured for URL: %s", req.URL)
}

Next Steps