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