Mocking & Test Doubles
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