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
- Learn about HTTP Testing
- Explore Integration Tests
- Study Test Coverage