Mocking & Test Doubles

6 minute read

Mocking Overview

Mocks simulate API behavior for fast unit tests without network calls or real infrastructure.

Manual Mocks

Simple Mock Client

package provider

type MockClient struct {
    GetCacheFunc    func(ctx context.Context, id string) (*client.Cache, error)
    CreateCacheFunc func(ctx context.Context, input client.CreateCacheInput) (*client.Cache, error)
    UpdateCacheFunc func(ctx context.Context, id string, input client.UpdateCacheInput) (*client.Cache, error)
    DeleteCacheFunc func(ctx context.Context, id string) error
}

func (m *MockClient) GetCache(ctx context.Context, id string) (*client.Cache, error) {
    if m.GetCacheFunc != nil {
        return m.GetCacheFunc(ctx, id)
    }
    return nil, fmt.Errorf("GetCacheFunc not implemented")
}

func (m *MockClient) CreateCache(ctx context.Context, input client.CreateCacheInput) (*client.Cache, error) {
    if m.CreateCacheFunc != nil {
        return m.CreateCacheFunc(ctx, input)
    }
    return nil, fmt.Errorf("CreateCacheFunc not implemented")
}

func (m *MockClient) UpdateCache(ctx context.Context, id string, input client.UpdateCacheInput) (*client.Cache, error) {
    if m.UpdateCacheFunc != nil {
        return m.UpdateCacheFunc(ctx, id, input)
    }
    return nil, fmt.Errorf("UpdateCacheFunc not implemented")
}

func (m *MockClient) DeleteCache(ctx context.Context, id string) error {
    if m.DeleteCacheFunc != nil {
        return m.DeleteCacheFunc(ctx, id)
    }
    return fmt.Errorf("DeleteCacheFunc not implemented")
}

Using Manual Mocks

func TestCacheResource_Create(t *testing.T) {
    mockClient := &MockClient{
        CreateCacheFunc: func(ctx context.Context, input client.CreateCacheInput) (*client.Cache, error) {
            return &client.Cache{
                ID:     "cache-123",
                Name:   input.Name,
                Engine: input.Engine,
                Status: "available",
            }, nil
        },
    }
    
    resource := &CacheResource{client: mockClient}
    
    // Test resource Create operation
    req := resource.CreateRequest{
        Plan: /* ... */,
    }
    resp := &resource.CreateResponse{}
    
    resource.Create(context.Background(), req, resp)
    
    assert.False(t, resp.Diagnostics.HasError())
}

Using testify/mock

Create Mock with testify

package provider

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

type MockClient struct {
    mock.Mock
}

func (m *MockClient) GetCache(ctx context.Context, id string) (*client.Cache, error) {
    args := m.Called(ctx, id)
    
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    
    return args.Get(0).(*client.Cache), args.Error(1)
}

func (m *MockClient) CreateCache(ctx context.Context, input client.CreateCacheInput) (*client.Cache, error) {
    args := m.Called(ctx, input)
    
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    
    return args.Get(0).(*client.Cache), args.Error(1)
}

func (m *MockClient) UpdateCache(ctx context.Context, id string, input client.UpdateCacheInput) (*client.Cache, error) {
    args := m.Called(ctx, id, input)
    
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    
    return args.Get(0).(*client.Cache), args.Error(1)
}

func (m *MockClient) DeleteCache(ctx context.Context, id string) error {
    args := m.Called(ctx, id)
    return args.Error(0)
}

Using testify Mocks

func TestCacheResource_Create_Success(t *testing.T) {
    mockClient := new(MockClient)
    
    expectedCache := &client.Cache{
        ID:     "cache-123",
        Name:   "test-cache",
        Engine: "redis",
        Status: "available",
    }
    
    // Set expectations
    mockClient.On("CreateCache", mock.Anything, mock.MatchedBy(func(input client.CreateCacheInput) bool {
        return input.Name == "test-cache" && input.Engine == "redis"
    })).Return(expectedCache, nil)
    
    resource := &CacheResource{client: mockClient}
    
    // Test Create operation
    // ... test code ...
    
    // Verify expectations were met
    mockClient.AssertExpectations(t)
}

func TestCacheResource_Create_APIError(t *testing.T) {
    mockClient := new(MockClient)
    
    // Return error
    mockClient.On("CreateCache", mock.Anything, mock.Anything).
        Return(nil, fmt.Errorf("API error: rate limit exceeded"))
    
    resource := &CacheResource{client: mockClient}
    
    // Test that error is handled properly
    // ... test code ...
    
    mockClient.AssertExpectations(t)
}

Mock HTTP Server

httptest Server

func TestClient_GetCache(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Verify request
        assert.Equal(t, "GET", r.Method)
        assert.Equal(t, "/v1/caches/cache-123", r.URL.Path)
        assert.Equal(t, "Bearer test-key", r.Header.Get("Authorization"))
        
        // Return response
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(client.Cache{
            ID:     "cache-123",
            Name:   "test-cache",
            Engine: "redis",
        })
    }))
    defer server.Close()
    
    // Create client with test server URL
    client, err := client.NewClient(&client.Config{
        APIKey:  "test-key",
        BaseURL: server.URL,
    })
    assert.NoError(t, err)
    
    // Test GetCache
    cache, err := client.GetCache(context.Background(), "cache-123")
    
    assert.NoError(t, err)
    assert.Equal(t, "cache-123", cache.ID)
    assert.Equal(t, "test-cache", cache.Name)
}

Test Different Responses

func TestClient_GetCache_NotFound(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusNotFound)
        json.NewEncoder(w).Encode(map[string]string{
            "error": "Cache not found",
        })
    }))
    defer server.Close()
    
    client, _ := client.NewClient(&client.Config{
        APIKey:  "test-key",
        BaseURL: server.URL,
    })
    
    _, err := client.GetCache(context.Background(), "cache-123")
    
    assert.Error(t, err)
    assert.True(t, client.IsNotFoundError(err))
}

func TestClient_GetCache_RateLimit(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-RateLimit-Limit", "100")
        w.Header().Set("X-RateLimit-Remaining", "0")
        w.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(1*time.Hour).Unix()))
        w.WriteHeader(http.StatusTooManyRequests)
    }))
    defer server.Close()
    
    client, _ := client.NewClient(&client.Config{
        APIKey:  "test-key",
        BaseURL: server.URL,
    })
    
    _, err := client.GetCache(context.Background(), "cache-123")
    
    assert.Error(t, err)
    assert.True(t, client.IsRateLimitError(err))
}

Test Fixtures

Shared Test Data

// test_fixtures.go
package provider

func testCache() *client.Cache {
    return &client.Cache{
        ID:            "cache-123",
        Name:          "test-cache",
        Engine:        "redis",
        EngineVersion: "7.0",
        NodeType:      "cache.t3.micro",
        NumNodes:      1,
        Port:          6379,
        Status:        "available",
        Endpoint:      "cache-123.cache.example.com:6379",
        CreatedAt:     time.Now().Add(-1 * time.Hour),
        UpdatedAt:     time.Now(),
    }
}

func testCacheWithOptions(opts map[string]interface{}) *client.Cache {
    cache := testCache()
    
    if name, ok := opts["name"].(string); ok {
        cache.Name = name
    }
    if engine, ok := opts["engine"].(string); ok {
        cache.Engine = engine
    }
    if numNodes, ok := opts["num_nodes"].(int); ok {
        cache.NumNodes = numNodes
    }
    
    return cache
}

// Usage
func TestCacheResource_Create(t *testing.T) {
    expectedCache := testCache()
    // ... use expectedCache in test
}

func TestCacheResource_MultiNode(t *testing.T) {
    expectedCache := testCacheWithOptions(map[string]interface{}{
        "num_nodes": 3,
    })
    // ... use expectedCache in test
}

Fixture Builders

type CacheBuilder struct {
    cache *client.Cache
}

func NewCacheBuilder() *CacheBuilder {
    return &CacheBuilder{
        cache: &client.Cache{
            ID:            "cache-123",
            Name:          "test-cache",
            Engine:        "redis",
            EngineVersion: "7.0",
            NodeType:      "cache.t3.micro",
            NumNodes:      1,
            Status:        "available",
        },
    }
}

func (b *CacheBuilder) WithName(name string) *CacheBuilder {
    b.cache.Name = name
    return b
}

func (b *CacheBuilder) WithEngine(engine string) *CacheBuilder {
    b.cache.Engine = engine
    return b
}

func (b *CacheBuilder) WithNodes(n int) *CacheBuilder {
    b.cache.NumNodes = n
    return b
}

func (b *CacheBuilder) Build() *client.Cache {
    return b.cache
}

// Usage
func TestCacheResource_MultiNode(t *testing.T) {
    cache := NewCacheBuilder().
        WithName("multi-node-cache").
        WithNodes(3).
        Build()
    
    // ... use cache in test
}

Spy Pattern

Track Function Calls

type SpyClient struct {
    GetCacheCalls    []string
    CreateCacheCalls []client.CreateCacheInput
    UpdateCacheCalls []struct {
        ID    string
        Input client.UpdateCacheInput
    }
    DeleteCacheCalls []string
}

func (s *SpyClient) GetCache(ctx context.Context, id string) (*client.Cache, error) {
    s.GetCacheCalls = append(s.GetCacheCalls, id)
    return testCache(), nil
}

func (s *SpyClient) CreateCache(ctx context.Context, input client.CreateCacheInput) (*client.Cache, error) {
    s.CreateCacheCalls = append(s.CreateCacheCalls, input)
    cache := testCache()
    cache.Name = input.Name
    return cache, nil
}

// Usage
func TestCacheResource_Create_CallsAPI(t *testing.T) {
    spyClient := &SpyClient{}
    resource := &CacheResource{client: spyClient}
    
    // Perform Create
    // ... test code ...
    
    // Verify API was called
    assert.Equal(t, 1, len(spyClient.CreateCacheCalls))
    assert.Equal(t, "test-cache", spyClient.CreateCacheCalls[0].Name)
}

Mock Generator Tools

Using mockery

# Install mockery
go install github.com/vektra/mockery/v2@latest

# Generate mocks
mockery --name=Client --output=./mocks --outpkg=mocks

Generated mock:

// Code generated by mockery. DO NOT EDIT.

package mocks

type Client struct {
    mock.Mock
}

func (m *Client) GetCache(ctx context.Context, id string) (*client.Cache, error) {
    ret := m.Called(ctx, id)
    
    var r0 *client.Cache
    if rf, ok := ret.Get(0).(func(context.Context, string) *client.Cache); ok {
        r0 = rf(ctx, id)
    } else {
        if ret.Get(0) != nil {
            r0 = ret.Get(0).(*client.Cache)
        }
    }
    
    var r1 error
    if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
        r1 = rf(ctx, id)
    } else {
        r1 = ret.Error(1)
    }
    
    return r0, r1
}

Best Practices

1. Use Interfaces

// ✅ Good: Interface allows mocking
type Client interface {
    GetCache(ctx context.Context, id string) (*Cache, error)
    CreateCache(ctx context.Context, input CreateCacheInput) (*Cache, error)
}

// ❌ Bad: Concrete type hard to mock
type Client struct {
    httpClient *http.Client
}

2. Keep Mocks Simple

// ✅ Good: Simple, focused mock
mockClient.On("GetCache", mock.Anything, "cache-123").
    Return(&client.Cache{ID: "cache-123"}, nil)

// ❌ Bad: Complex mock logic
mockClient.On("GetCache", mock.Anything, mock.Anything).
    Return(func(ctx context.Context, id string) (*client.Cache, error) {
        // Complex logic here...
    })

3. Verify Expectations

// ✅ Good: Verify mock was called correctly
mockClient.On("CreateCache", mock.Anything, expectedInput).
    Return(expectedCache, nil)

// ... test code ...

mockClient.AssertExpectations(t)

// ❌ Bad: No verification
mockClient.On("CreateCache", mock.Anything, mock.Anything).
    Return(expectedCache, nil)

Key Takeaways

  • ✅ Use mocks for fast, isolated unit tests
  • ✅ Create interface for mockable clients
  • ✅ Use testify/mock for complex mocking
  • ✅ Use httptest for HTTP client testing
  • ✅ Create test fixtures for common data
  • ✅ Use builder pattern for flexible fixtures
  • ✅ Verify mock expectations
  • ✅ Keep mocks simple and focused
  • ✅ Generate mocks with tools like mockery
  • ✅ Combine unit tests (mocks) with acceptance tests (real API)

What’s Next?

Learn how to create reusable test fixtures and helper functions.

Next: Test Fixtures & Helpers →


Previous: ← Testing Data Sources

Last updated: July 3, 2026

Updated: