Skip to main content

Mocking & Interface Testing

In Go, interfaces and mocks are used to isolate external dependencies (DB, APIs, file systems) in tests.


Why Mocks Are Needed

Unit tests must be fast and predictable. Real databases and external APIs:

  • are slow (network round trips)
  • are unreliable (external service downtime)
  • have state dependencies (test order affects results)

Mocks solve this problem through interfaces.


Interface-Based Design

// ✅ Define dependencies as interfaces
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*User, error)
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id int64) error
}

type EmailService interface {
SendWelcome(email, name string) error
SendPasswordReset(email, token string) error
}

// Service depends on interfaces, not implementations
type UserService struct {
repo UserRepository
email EmailService
}

func NewUserService(repo UserRepository, email EmailService) *UserService {
return &UserService{repo: repo, email: email}
}

func (s *UserService) Register(ctx context.Context, name, email, password string) (*User, error) {
// Check for duplicate email
existing, err := s.repo.FindByEmail(ctx, email)
if err != nil {
return nil, fmt.Errorf("email check failed: %w", err)
}
if existing != nil {
return nil, fmt.Errorf("email already in use")
}

user := &User{Name: name, Email: email}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("user creation failed: %w", err)
}

// Send welcome email (failure doesn't prevent signup)
_ = s.email.SendWelcome(email, name)

return user, nil
}

Manual Mock Implementation

The simplest approach: write a test struct that implements the interface directly.

// Manual mock: implements UserRepository interface
type mockUserRepository struct {
users map[int64]*User
nextID int64
// Fields for error simulation
findError error
createError error
callCount map[string]int
}

func newMockUserRepo() *mockUserRepository {
return &mockUserRepository{
users: make(map[int64]*User),
nextID: 1,
callCount: make(map[string]int),
}
}

func (m *mockUserRepository) FindByID(ctx context.Context, id int64) (*User, error) {
m.callCount["FindByID"]++
if m.findError != nil {
return nil, m.findError
}
user, ok := m.users[id]
if !ok {
return nil, nil
}
return user, nil
}

func (m *mockUserRepository) Create(ctx context.Context, user *User) error {
m.callCount["Create"]++
if m.createError != nil {
return m.createError
}
user.ID = m.nextID
m.users[m.nextID] = user
m.nextID++
return nil
}

// mockEmailService
type mockEmailService struct {
sentEmails []struct{ To, Subject string }
sendError error
}

func (m *mockEmailService) SendWelcome(email, name string) error {
m.sentEmails = append(m.sentEmails, struct{ To, Subject string }{email, "welcome"})
return m.sendError
}

func (m *mockEmailService) SendPasswordReset(email, token string) error {
m.sentEmails = append(m.sentEmails, struct{ To, Subject string }{email, "reset"})
return m.sendError
}

Using Manual Mocks

func TestUserService_Register(t *testing.T) {
t.Run("successful registration", func(t *testing.T) {
repo := newMockUserRepo()
emailSvc := &mockEmailService{}
svc := NewUserService(repo, emailSvc)

user, err := svc.Register(context.Background(), "Kim Golang", "golang@example.com", "pass")

assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, "Kim Golang", user.Name)
assert.Equal(t, 1, repo.callCount["Create"])
assert.Len(t, emailSvc.sentEmails, 1)
})

t.Run("fails on DB error", func(t *testing.T) {
repo := newMockUserRepo()
repo.createError = errors.New("DB error")
emailSvc := &mockEmailService{}
svc := NewUserService(repo, emailSvc)

_, err := svc.Register(context.Background(), "test", "test@example.com", "pass")

assert.Error(t, err)
assert.Contains(t, err.Error(), "user creation failed")
// Email should not be sent
assert.Empty(t, emailSvc.sentEmails)
})
}

testify/mock — Automatic Mock Generation

testify/mock makes it easy to verify calls, match arguments, and set return values.

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

// Mock struct: embeds mock.Mock
type MockUserRepository struct {
mock.Mock
}

func (m *MockUserRepository) FindByID(ctx context.Context, id int64) (*User, error) {
// Called() records the call and retrieves return values
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}

func (m *MockUserRepository) Create(ctx context.Context, user *User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
func TestWithTestifyMock(t *testing.T) {
ctx := context.Background()
repo := new(MockUserRepository)
emailSvc := new(MockEmailService)

// FindByEmail: return nil (no duplicate)
repo.On("FindByEmail", ctx, "golang@example.com").Return(nil, nil)

// Create: success (set user.ID to 1 using Run)
repo.On("Create", ctx, mock.AnythingOfType("*main.User")).
Run(func(args mock.Arguments) {
user := args.Get(1).(*User)
user.ID = 1
}).
Return(nil)

emailSvc.On("SendWelcome", "golang@example.com", "Kim Golang").Return(nil)

svc := NewUserService(repo, emailSvc)
user, err := svc.Register(ctx, "Kim Golang", "golang@example.com", "pass")

assert.NoError(t, err)
assert.Equal(t, int64(1), user.ID)

// Verify all configured calls were made
repo.AssertExpectations(t)
emailSvc.AssertExpectations(t)

// Verify specific call count
repo.AssertNumberOfCalls(t, "Create", 1)
}

gomock — Code-Generation-Based Mocks

go install go.uber.org/mock/mockgen@latest
go get go.uber.org/mock/gomock

Auto-Generate from Interface File

// interfaces.go
package service

//go:generate mockgen -source=interfaces.go -destination=mocks/mock_interfaces.go -package=mocks

type UserRepository interface {
FindByID(ctx context.Context, id int64) (*User, error)
Create(ctx context.Context, user *User) error
}
go generate ./...

Using generated mocks:

import (
"testing"
"go.uber.org/mock/gomock"
"myapp/mocks"
)

func TestWithGomock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish() // verify no unexpected calls

repo := mocks.NewMockUserRepository(ctrl)

// Set expectations
repo.EXPECT().
FindByID(gomock.Any(), int64(1)).
Return(&User{ID: 1, Name: "test"}, nil).
Times(1)

svc := NewUserService(repo, nil)
user, err := svc.GetUser(context.Background(), 1)

assert.NoError(t, err)
assert.Equal(t, "test", user.Name)
}

Dependency Injection Pattern — Maximize Testability

// Dependency injection via function type
type NotifyFunc func(userID int64, message string) error

type OrderService struct {
db OrderRepository
notify NotifyFunc
}

func NewOrderService(db OrderRepository, notify NotifyFunc) *OrderService {
return &OrderService{db: db, notify: notify}
}

// Replace function in tests
func TestOrderService(t *testing.T) {
var notified []struct{ ID int64; Msg string }

mockNotify := func(userID int64, message string) error {
notified = append(notified, struct{ ID int64; Msg string }{userID, message})
return nil
}

svc := NewOrderService(mockRepo, mockNotify)
svc.PlaceOrder(context.Background(), 1, 100.0)

assert.Len(t, notified, 1)
assert.Equal(t, int64(1), notified[0].ID)
}

Key Summary

ApproachAdvantagesDisadvantages
Manual MockSimple, no dependenciesLots of boilerplate
testify/mockFlexible argument matchingRuntime verification
gomockCompile-time type checkingCode generation required

Interface Design Principles:

  • Interfaces defined by the consumer, not the provider
  • Keep interfaces small — 1–3 methods is ideal
  • Consider interfaces for testing even if only one implementation exists