Mocking #

Mocking is the technique of replacing real dependencies (databases, HTTP APIs, file systems) with fake versions whose behavior you can control in tests. Without mocking, your tests depend on external infrastructure: the database must be running, the API must be online, files must exist. With mocks, tests can run fast, deterministic, and offline. In Go, mocking is very natural because of interfaces — as long as a dependency is accessed through an interface, you can swap the real implementation for a mock implementation at any time.

The difference in dependency structure between integration testing (with a real database) and unit testing (using Mocking via Interfaces) can be visualized in the following diagram:

flowchart TD
    subgraph RealTest["Real Testing (Real Dependency - Slow & Flaky)"]
        USReal["UserService"] -->|"Run Query"| DBReal[("Real Postgres Database")]
    end

    subgraph MockTest["Unit Testing (Mocking - Fast & Isolated)"]
        USMock["UserService"] -->|"Interface"| RepoIntf["UserRepository (Interface)"]
        RepoIntf -->|"Inject"| MockDB["UserRepositoryMock (Fake / Controlled)"]
        
        style MockDB fill:#eef9ff,stroke:#007acc,stroke-width:2px
    end

Interfaces as the Foundation of Testability #

Code that’s easy to mock is code whose dependencies are interfaces, not concrete types:

// HARD to mock: dependency directly on a concrete type
type UserService struct {
    db *sql.DB  // concrete — can't be swapped during tests
}

// EASY to mock: dependency through an interface
type UserRepository interface {
    FindByID(id int) (*User, error)
    Save(user *User) error
    Delete(id int) error
}

type UserService struct {
    repo UserRepository  // interface — can be swapped during tests
}

The principle: inject dependencies through the constructor, don’t create dependencies inside the struct:

// ANTI-PATTERN: dependency created inside — can't be mocked
func NewUserService() *UserService {
    return &UserService{
        repo: NewPostgresRepository(),  // hardcoded!
    }
}

// CORRECT: dependency injected from outside
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

// Production
svc := NewUserService(NewPostgresRepository(db))

// Test
svc := NewUserService(&MockRepository{})

Stubs, Mocks, Fakes, and Spies #

Before getting into the implementation, it’s important to understand the terminology:

STUB
  A minimal implementation that returns fixed values.
  Doesn't verify how it's called.
  Good for: "give this value so the function under test can run"

MOCK
  An implementation that verifies call expectations.
  Can check: was it called? how many times? with what arguments?
  Good for: "make sure this function calls the dependency correctly"

FAKE
  An implementation that actually works but is simpler.
  Example: an in-memory database as a replacement for PostgreSQL.
  Good for: lighter integration tests

SPY
  Wraps the real implementation and records all calls.
  Good for: verifying interactions without changing the original behavior

Manual Mocks — The Simplest Way #

For simple interfaces, create a struct that implements them manually:

// The interface to mock
type EmailSender interface {
    Send(to, subject, body string) error
}

// Manual mock — a struct with fields to control behavior
type MockEmailSender struct {
    // Control the return values
    ShouldFail bool
    ReturnErr  error

    // Spy — record all calls
    Calls []struct {
        To      string
        Subject string
        Body    string
    }
}

func (m *MockEmailSender) Send(to, subject, body string) error {
    // Record the call
    m.Calls = append(m.Calls, struct {
        To      string
        Subject string
        Body    string
    }{to, subject, body})

    if m.ShouldFail {
        if m.ReturnErr != nil {
            return m.ReturnErr
        }
        return errors.New("email failed to send")
    }
    return nil
}

// Helper methods for assertions
func (m *MockEmailSender) CallCount() int {
    return len(m.Calls)
}

func (m *MockEmailSender) LastCall() (to, subject, body string) {
    if len(m.Calls) == 0 {
        return "", "", ""
    }
    last := m.Calls[len(m.Calls)-1]
    return last.To, last.Subject, last.Body
}

// Usage in a test
func TestRegisterUser(t *testing.T) {
    mockEmail := &MockEmailSender{}
    svc := NewUserService(mockEmail)

    err := svc.Register("[email protected]", "password123")
    if err != nil {
        t.Fatal(err)
    }

    // Verify the email was sent
    if mockEmail.CallCount() != 1 {
        t.Errorf("email sent %d times, want 1", mockEmail.CallCount())
    }

    to, subject, _ := mockEmail.LastCall()
    if to != "[email protected]" {
        t.Errorf("email sent to %q, want %q", to, "[email protected]")
    }
    if !strings.Contains(subject, "Verification") {
        t.Errorf("subject %q doesn't contain 'Verification'", subject)
    }
}

// Test with an error
func TestRegisterUser_EmailFail(t *testing.T) {
    mockEmail := &MockEmailSender{
        ShouldFail: true,
        ReturnErr:  errors.New("SMTP server down"),
    }
    svc := NewUserService(mockEmail)

    err := svc.Register("[email protected]", "password123")
    if err == nil {
        t.Fatal("should error if the email fails")
    }
}

Functional Mocks — Mocks with Function Fields #

A more flexible alternative: store the behavior as function fields:

type MockNotifier struct {
    NotifyFn func(userID int, msg string) error
}

func (m *MockNotifier) Notify(userID int, msg string) error {
    if m.NotifyFn != nil {
        return m.NotifyFn(userID, msg)
    }
    return nil  // no-op default
}

// Test with custom behavior per test case
func TestProcessOrder(t *testing.T) {
    t.Run("notification succeeds", func(t *testing.T) {
        notified := false
        mock := &MockNotifier{
            NotifyFn: func(userID int, msg string) error {
                notified = true
                return nil
            },
        }
        svc := NewOrderService(mock)
        svc.Process(orderID)

        if !notified {
            t.Error("should have sent a notification")
        }
    })

    t.Run("continue even if the notification fails", func(t *testing.T) {
        mock := &MockNotifier{
            NotifyFn: func(userID int, msg string) error {
                return errors.New("notification service down")
            },
        }
        svc := NewOrderService(mock)
        err := svc.Process(orderID)

        // The order should still succeed even if the notification fails
        if err != nil {
            t.Errorf("order failed even though the notification isn't required: %v", err)
        }
    })
}

Mocking HTTP Clients #

To mock HTTP calls, implement http.RoundTripper:

// MockTransport records requests and returns a configured response
type MockTransport struct {
    Requests  []*http.Request
    Response  *http.Response
    Err       error
}

func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    m.Requests = append(m.Requests, req)
    if m.Err != nil {
        return nil, m.Err
    }
    return m.Response, nil
}

// Helper to create a mock response
func mockResponse(statusCode int, body string) *http.Response {
    return &http.Response{
        StatusCode: statusCode,
        Body:       io.NopCloser(strings.NewReader(body)),
        Header:     make(http.Header),
    }
}

// Usage
func TestFetchUser(t *testing.T) {
    transport := &MockTransport{
        Response: mockResponse(200, `{"id":1,"name":"Budi"}`),
    }

    client := &http.Client{Transport: transport}
    svc := NewUserAPIClient(client)

    user, err := svc.FetchUser(1)
    if err != nil {
        t.Fatal(err)
    }
    if user.Name != "Budi" {
        t.Errorf("name = %q, want %q", user.Name, "Budi")
    }

    // Verify the request that was sent
    if len(transport.Requests) != 1 {
        t.Errorf("request sent %d times, want 1", len(transport.Requests))
    }
    req := transport.Requests[0]
    if req.Method != http.MethodGet {
        t.Errorf("method = %q, want GET", req.Method)
    }
    if !strings.HasSuffix(req.URL.Path, "/users/1") {
        t.Errorf("path = %q, doesn't contain /users/1", req.URL.Path)
    }
}

// Test the error scenario
func TestFetchUser_NetworkError(t *testing.T) {
    transport := &MockTransport{
        Err: errors.New("connection refused"),
    }
    client := &http.Client{Transport: transport}
    svc := NewUserAPIClient(client)

    _, err := svc.FetchUser(1)
    if err == nil {
        t.Fatal("should have errored")
    }
}

testify/mock — Mocks with Expectations #

The testify library provides more expressive mocks with an expectation system:

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

// Mock definition
type MockUserRepo struct {
    mock.Mock
}

func (m *MockUserRepo) FindByID(id int) (*User, error) {
    args := m.Called(id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*User), args.Error(1)
}

func (m *MockUserRepo) Save(user *User) error {
    args := m.Called(user)
    return args.Error(0)
}

func (m *MockUserRepo) Delete(id int) error {
    args := m.Called(id)
    return args.Error(0)
}

// Test with expectations
func TestGetUser(t *testing.T) {
    mockRepo := new(MockUserRepo)

    // Set the expectation: FindByID(42) must be called once
    // and return this user
    expectedUser := &User{ID: 42, Name: "Budi"}
    mockRepo.On("FindByID", 42).Return(expectedUser, nil).Once()

    svc := NewUserService(mockRepo)
    user, err := svc.GetUser(42)

    // Assert the result
    assert.NoError(t, err)
    assert.Equal(t, "Budi", user.Name)

    // Verify all expectations were met
    mockRepo.AssertExpectations(t)
}

func TestGetUser_NotFound(t *testing.T) {
    mockRepo := new(MockUserRepo)
    mockRepo.On("FindByID", 999).Return(nil, ErrNotFound)

    svc := NewUserService(mockRepo)
    _, err := svc.GetUser(999)

    assert.ErrorIs(t, err, ErrNotFound)
    mockRepo.AssertExpectations(t)
}

// Expectations with matchers
func TestSaveUser(t *testing.T) {
    mockRepo := new(MockUserRepo)

    // Accept any User (mock.AnythingOfType or mock.MatchedBy)
    mockRepo.On("Save", mock.MatchedBy(func(u *User) bool {
        return u.Email != "" // validation: email must not be empty
    })).Return(nil)

    svc := NewUserService(mockRepo)
    err := svc.CreateUser("Budi", "[email protected]")

    assert.NoError(t, err)
    mockRepo.AssertExpectations(t)
}

gomock — Mock Generators #

gomock from Google generates mocks automatically from interfaces:

go install github.com/golang/mock/mockgen@latest
// repository.go
//go:generate mockgen -source=repository.go -destination=mock/repository_mock.go -package=mock

type PaymentRepository interface {
    FindTransaction(id string) (*Transaction, error)
    SaveTransaction(tx *Transaction) error
    UpdateStatus(id string, status string) error
}
# Generate the mock
go generate ./...

# Result: mock/repository_mock.go
// Using the generated mock
import "myapp/mock"

func TestProcessPayment(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()  // verify all expectations at the end

    mockRepo := mock.NewMockPaymentRepository(ctrl)

    // Set expectations
    mockRepo.EXPECT().
        FindTransaction("TRX-001").
        Return(&Transaction{ID: "TRX-001", Amount: 100_000}, nil).
        Times(1)

    mockRepo.EXPECT().
        UpdateStatus("TRX-001", "SUCCESS").
        Return(nil).
        Times(1)

    svc := NewPaymentService(mockRepo)
    err := svc.Process("TRX-001")
    assert.NoError(t, err)
    // ctrl.Finish() automatically verifies all expectations
}

Complete Example Program #

The following program simulates an e-commerce service with a mock repository, mock notifier, and mock payment gateway:

package ecommerce_test

import (
    "errors"
    "testing"
    "time"
)

// ── Domain ────────────────────────────────────────────────────

type Order struct {
    ID        string
    UserID    int
    Items     []OrderItem
    Total     float64
    Status    string
    CreatedAt time.Time
}

type OrderItem struct {
    ProductID int
    Qty       int
    Price     float64
}

var (
    ErrOrderNotFound     = errors.New("order not found")
    ErrPaymentFailed     = errors.New("payment failed")
    ErrStockInsufficient = errors.New("insufficient stock")
)

// ── Interfaces ────────────────────────────────────────────────

type OrderRepository interface {
    FindByID(id string) (*Order, error)
    Save(order *Order) error
    UpdateStatus(id, status string) error
}

type PaymentGateway interface {
    Charge(orderID string, amount float64) (string, error) // returns payment ID
}

type Notifier interface {
    Notify(userID int, message string) error
}

type StockChecker interface {
    IsAvailable(productID, qty int) (bool, error)
}

// ── Service ───────────────────────────────────────────────────

type OrderService struct {
    repo     OrderRepository
    payment  PaymentGateway
    notifier Notifier
    stock    StockChecker
}

func NewOrderService(
    repo OrderRepository,
    payment PaymentGateway,
    notifier Notifier,
    stock StockChecker,
) *OrderService {
    return &OrderService{repo, payment, notifier, stock}
}

func (s *OrderService) Checkout(order *Order) error {
    // Check the stock of every item
    for _, item := range order.Items {
        ok, err := s.stock.IsAvailable(item.ProductID, item.Qty)
        if err != nil {
            return fmt.Errorf("check stock: %w", err)
        }
        if !ok {
            return ErrStockInsufficient
        }
    }

    // Save the order
    order.Status = "PENDING"
    if err := s.repo.Save(order); err != nil {
        return fmt.Errorf("save order: %w", err)
    }

    // Process the payment
    _, err := s.payment.Charge(order.ID, order.Total)
    if err != nil {
        _ = s.repo.UpdateStatus(order.ID, "PAYMENT_FAILED")
        return ErrPaymentFailed
    }

    // Update the status
    if err := s.repo.UpdateStatus(order.ID, "PAID"); err != nil {
        return fmt.Errorf("update status: %w", err)
    }

    // Send the notification (no need to return an error)
    _ = s.notifier.Notify(order.UserID, "Your order was paid successfully!")

    return nil
}

// ── Mocks ─────────────────────────────────────────────────────

type mockOrderRepo struct {
    orders      map[string]*Order
    saveCalled  int
    updateLog   []struct{ id, status string }
    saveFails   bool
    updateFails bool
}

func newMockOrderRepo() *mockOrderRepo {
    return &mockOrderRepo{orders: make(map[string]*Order)}
}

func (m *mockOrderRepo) FindByID(id string) (*Order, error) {
    o, ok := m.orders[id]
    if !ok {
        return nil, ErrOrderNotFound
    }
    return o, nil
}

func (m *mockOrderRepo) Save(order *Order) error {
    if m.saveFails {
        return errors.New("db error")
    }
    m.saveCalled++
    m.orders[order.ID] = order
    return nil
}

func (m *mockOrderRepo) UpdateStatus(id, status string) error {
    if m.updateFails {
        return errors.New("db error")
    }
    m.updateLog = append(m.updateLog, struct{ id, status string }{id, status})
    if o, ok := m.orders[id]; ok {
        o.Status = status
    }
    return nil
}

type mockPaymentGateway struct {
    shouldFail bool
    charged    []struct{ orderID string; amount float64 }
}

func (m *mockPaymentGateway) Charge(orderID string, amount float64) (string, error) {
    m.charged = append(m.charged, struct{ orderID string; amount float64 }{orderID, amount})
    if m.shouldFail {
        return "", errors.New("card declined")
    }
    return "PAY-" + orderID, nil
}

type mockNotifier struct {
    notifications []struct{ userID int; msg string }
    shouldFail    bool
}

func (m *mockNotifier) Notify(userID int, msg string) error {
    m.notifications = append(m.notifications, struct{ userID int; msg string }{userID, msg})
    if m.shouldFail {
        return errors.New("notification failed")
    }
    return nil
}

type mockStockChecker struct {
    available map[int]bool
}

func (m *mockStockChecker) IsAvailable(productID, qty int) (bool, error) {
    return m.available[productID], nil
}

// ── Tests ─────────────────────────────────────────────────────

func makeOrder() *Order {
    return &Order{
        ID:     "ORD-001",
        UserID: 42,
        Items:  []OrderItem{{ProductID: 1, Qty: 2, Price: 50_000}},
        Total:  100_000,
    }
}

func TestCheckout_Success(t *testing.T) {
    repo := newMockOrderRepo()
    pay := &mockPaymentGateway{}
    notif := &mockNotifier{}
    stock := &mockStockChecker{available: map[int]bool{1: true}}

    svc := NewOrderService(repo, pay, notif, stock)
    order := makeOrder()

    err := svc.Checkout(order)
    if err != nil {
        t.Fatalf("checkout failed: %v", err)
    }

    // Verify the repo
    if repo.saveCalled != 1 {
        t.Errorf("Save() called %d times, want 1", repo.saveCalled)
    }
    savedOrder, _ := repo.FindByID("ORD-001")
    if savedOrder.Status != "PAID" {
        t.Errorf("status = %q, want PAID", savedOrder.Status)
    }

    // Verify the payment
    if len(pay.charged) != 1 {
        t.Errorf("Charge() called %d times, want 1", len(pay.charged))
    }
    if pay.charged[0].amount != 100_000 {
        t.Errorf("amount = %.0f, want 100000", pay.charged[0].amount)
    }

    // Verify the notification
    if len(notif.notifications) != 1 {
        t.Errorf("Notify() called %d times, want 1", len(notif.notifications))
    }
}

func TestCheckout_StockInsufficient(t *testing.T) {
    repo := newMockOrderRepo()
    pay := &mockPaymentGateway{}
    notif := &mockNotifier{}
    stock := &mockStockChecker{available: map[int]bool{1: false}} // out of stock

    svc := NewOrderService(repo, pay, notif, stock)
    err := svc.Checkout(makeOrder())

    if !errors.Is(err, ErrStockInsufficient) {
        t.Errorf("error = %v, want ErrStockInsufficient", err)
    }
    // Make sure nothing was called
    if repo.saveCalled != 0 {
        t.Error("Save() should not be called if the stock is insufficient")
    }
    if len(pay.charged) != 0 {
        t.Error("Charge() should not be called if the stock is insufficient")
    }
}

func TestCheckout_PaymentFailed(t *testing.T) {
    repo := newMockOrderRepo()
    pay := &mockPaymentGateway{shouldFail: true}
    notif := &mockNotifier{}
    stock := &mockStockChecker{available: map[int]bool{1: true}}

    svc := NewOrderService(repo, pay, notif, stock)
    err := svc.Checkout(makeOrder())

    if !errors.Is(err, ErrPaymentFailed) {
        t.Errorf("error = %v, want ErrPaymentFailed", err)
    }

    // The order must be saved with the PAYMENT_FAILED status
    savedOrder, _ := repo.FindByID("ORD-001")
    if savedOrder == nil || savedOrder.Status != "PAYMENT_FAILED" {
        t.Errorf("status = %v, want PAYMENT_FAILED", savedOrder)
    }

    // No notification should be sent
    if len(notif.notifications) != 0 {
        t.Error("Notify() should not be called if the payment fails")
    }
}

func TestCheckout_NotificationFailedIsOK(t *testing.T) {
    // The checkout must still succeed even if the notification fails
    repo := newMockOrderRepo()
    pay := &mockPaymentGateway{}
    notif := &mockNotifier{shouldFail: true}
    stock := &mockStockChecker{available: map[int]bool{1: true}}

    svc := NewOrderService(repo, pay, notif, stock)
    err := svc.Checkout(makeOrder())

    if err != nil {
        t.Errorf("checkout failed even though the notification failed: %v", err)
    }
}

Summary #

  • Interfaces are the foundation of testability — dependencies must be interfaces, not concrete types.
  • Dependency injection through constructors — don’t create dependencies inside the struct.
  • Manual mocks are enough for small interfaces — a struct implementing the interface with fields to control behavior.
  • Functional mocks (fields of func type) are more flexible — behavior can be customized per test case.
  • Mock HTTP clients via http.RoundTripper — no real server needed to test HTTP calls.
  • testify/mock for expressive expectations — On, Return, Once, Times, AssertExpectations.
  • gomock generates mocks automatically from interfaces — no manual writing needed.
  • //go:generate mockgen ... regenerates mocks when interfaces change.
  • Stub = fixed return values; Mock = verifies expectations; Fake = real but simple implementation; Spy = records without changing behavior.
  • Tests should fail for clear reasons — every mock verification should have an informative error message.

← Previous: Unit Testing   Next: JSON →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact