Pattern Name: Function Field Mock Pattern
Also known as:
- Manual Mocking Pattern
- Stub Functions Pattern
- First-Class Function Mocking
Functional Programming Concepts Used
1. First-Class Functions
Functions in Go are first-class citizens - they can be:
- Assigned to variables and struct fields
- Passed as arguments to other functions
- Returned from functions
type mockTaskService struct {
createRecordFunc func(ctx context.Context, userID uuid.UUID, req CreateRecordRequest) (*RecordResponse, error)
// ^ Function stored in a struct field (first-class function)
}2. Higher-Order Functions
Functions that take other functions as parameters or return functions.
setupMock: func(m *mockTaskService) {
// ^ Higher-order function: receives mock and configures its behavior
m.createRecordFunc = func(...) (*RecordResponse, error) {
// Function that will be called later
return &RecordResponse{}, nil
}
}Both concepts work together to enable flexible, per-test mock configuration.
Table-Driven Testing Pattern
Structure
testCases := []struct {
name string // Descriptive test name
requestBody any // Input data
setupContext func(*gin.Context) // Auth/context setup
setupMock func(*mockTaskService) // Mock behavior
expectedStatusCode int // Expected HTTP status
validateResponse func(*testing.T, *httptest.ResponseRecorder) // Custom validation
}{
// Test cases...
}Execution Loop
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 1. Setup mock
mockService := &mockTaskService{}
tc.setupMock(mockService)
// 2. Setup router and context
router := setupTestRouter()
router.POST("/records", func(c *gin.Context) {
tc.setupContext(c)
h.CreateRecord(c)
})
// 3. Execute request
req := httptest.NewRequest(http.MethodPost, "/records", body)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// 4. Validate response
assert.Equal(t, tc.expectedStatusCode, w.Code)
tc.validateResponse(t, w)
})
}Mock Implementation Pattern
1. Define Mock Struct
type mockTaskService struct {
// Function field for each interface method
createRecordFunc func(ctx context.Context, userID uuid.UUID, req CreateRecordRequest) (*RecordResponse, error)
deleteRecordFunc func(ctx context.Context, userID uuid.UUID, recordID uuid.UUID) error
getTodayTasksFunc func(ctx context.Context, userID uuid.UUID, date string) ([]TodayTaskResponse, error)
}2. Implement Interface Methods
Each method delegates to its corresponding function field:
func (m *mockTaskService) CreateRecord(ctx context.Context, userID uuid.UUID, req CreateRecordRequest) (*RecordResponse, error) {
if m.createRecordFunc != nil {
return m.createRecordFunc(ctx, userID, req)
}
return nil, errors.New("not implemented")
}Pattern: Check if function field is set → call it → fallback to error
3. Configure Mock Behavior Per Test
setupMock: func(m *mockTaskService) {
m.createRecordFunc = func(ctx context.Context, userID uuid.UUID, req CreateRecordRequest) (*RecordResponse, error) {
// Custom behavior for this test
return &RecordResponse{
ID: testRecordID,
TaskID: req.TaskID,
CheckInDate: req.CheckInDate,
}, nil
}
}Test Case Categories
✅ Success Cases
{
name: "should_return_created_when_record_created_successfully",
setupMock: func(m *mockTaskService) {
m.createRecordFunc = func(...) (*RecordResponse, error) {
return &RecordResponse{ID: testID}, nil
}
},
expectedStatusCode: http.StatusCreated,
}❌ Authentication Failures
{
name: "should_return_unauthorized_when_user_id_not_in_context",
setupContext: func(c *gin.Context) {
// Don't set user_id
},
expectedStatusCode: http.StatusUnauthorized,
}❌ Validation Errors
{
name: "should_return_bad_request_when_invalid_record_data",
setupMock: func(m *mockTaskService) {
m.createRecordFunc = func(...) (*RecordResponse, error) {
return nil, ErrInvalidRecordData
}
},
expectedStatusCode: http.StatusBadRequest,
}❌ Business Logic Errors
{
name: "should_return_conflict_when_duplicate_record",
setupMock: func(m *mockTaskService) {
m.createRecordFunc = func(...) (*RecordResponse, error) {
return nil, ErrDuplicateRecord
}
},
expectedStatusCode: http.StatusConflict,
}❌ Infrastructure Errors
{
name: "should_return_internal_server_error_when_service_fails",
setupMock: func(m *mockTaskService) {
m.createRecordFunc = func(...) (*RecordResponse, error) {
return nil, errors.New("database error")
}
},
expectedStatusCode: http.StatusInternalServerError,
}Advantages
🎯 Maintainability
- Adding new test cases = adding to slice
- No code duplication
- Easy to modify existing tests
📖 Readability
- Test names follow clear convention:
should_return_X_when_Y - Each test case is self-contained
- Easy to understand what’s being tested
🔍 Test Coverage
- Systematic coverage of all scenarios:
- Success paths
- Authentication failures
- Validation errors
- Business logic errors
- Infrastructure failures
🔒 Isolation
- Each test has independent mock behavior
- No shared state between tests
- Clear test boundaries
♻️ DRY (Don’t Repeat Yourself)
- No repeated setup/teardown code
- Shared validation logic where appropriate
- Reusable test infrastructure
Comparison: Mock Implementation Approaches
| Pattern | Implementation | Pros | Cons | Use When |
|---|---|---|---|---|
| Function Fields | m.createRecordFunc = func(...) {...} | ✅ Simple ✅ No dependencies ✅ Flexible ✅ Type-safe | ❌ More boilerplate ❌ Manual implementation | Small-medium projects, prefer simplicity |
| gomock | EXPECT().CreateRecord(gomock.Any()) | ✅ Auto-generated ✅ Powerful assertions ✅ Call ordering | ❌ External dependency ❌ Complex setup ❌ Generated code | Large projects, strict call verification |
| testify/mock | mock.On("CreateRecord").Return(...) | ✅ Popular ✅ Feature-rich ✅ Assertion helpers | ❌ Runtime reflection ❌ Magic strings ❌ Not type-safe | When using testify suite |
| Hard-coded | Fixed return values | ✅ Very simple | ❌ Not flexible ❌ Can’t vary per test | Simple scenarios only |
HTTP Handler Testing Pattern
Components
- httptest.ResponseRecorder: Captures HTTP response
- httptest.NewRequest: Creates test HTTP request
- Gin test router: Simulates routing without real server
- Context injection: Injects authentication/user data
Example Flow
// 1. Create mock service
mockService := &mockTaskService{}
mockService.createRecordFunc = func(...) (*RecordResponse, error) {
return &RecordResponse{ID: uuid.New()}, nil
}
// 2. Setup handler and router
h := NewHandler(mockService, &mockDBTXProvider{})
router := setupTestRouter()
router.POST("/records", func(c *gin.Context) {
c.Set("user_id", testUserID) // Inject context
h.CreateRecord(c) // Call handler
})
// 3. Create and execute request
body, _ := json.Marshal(requestBody)
req := httptest.NewRequest(http.MethodPost, "/records", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// 4. Validate response
assert.Equal(t, http.StatusCreated, w.Code)
var response RecordResponse
json.Unmarshal(w.Body.Bytes(), &response)
assert.Equal(t, testRecordID, response.ID)Naming Conventions
Test Function Names
Test{HandlerName}_{MethodName}
Example: TestHandler_CreateRecord
Test Case Names
should_return_{outcome}_when_{condition}
Examples:
should_return_created_when_record_created_successfullyshould_return_unauthorized_when_user_id_not_in_contextshould_return_conflict_when_duplicate_record
Best Practices
✅ Do’s
- Test all error paths: Success, validation, auth, business logic, infrastructure
- Use descriptive names: Make test failures self-explanatory
- Keep tests independent: No shared state between test cases
- Validate responses thoroughly: Check status code AND response body
- Use table-driven tests: When testing multiple scenarios for same handler
❌ Don’ts
- Don’t share mocks: Each test case should configure its own mock
- Don’t skip edge cases: Test empty arrays, nil values, invalid UUIDs
- Don’t test implementation details: Test behavior, not internals
- Don’t use real dependencies: Always mock external services/databases
Advanced Techniques
Assertions Inside Mocks
setupMock: func(m *mockTaskService) {
m.createRecordFunc = func(ctx context.Context, userID uuid.UUID, req CreateRecordRequest) (*RecordResponse, error) {
// Validate inputs inside the mock
assert.Equal(t, expectedUserID, userID)
assert.Equal(t, "2026-01-25", req.CheckInDate)
return &RecordResponse{}, nil
}
}Parameterized Validation
validateResponse: func(t *testing.T, w *httptest.ResponseRecorder) {
var response RecordResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, testRecordID, response.ID)
assert.Equal(t, testTaskID, response.TaskID)
assert.Equal(t, "2026-01-25", response.CheckInDate)
}Query Parameter Testing
{
name: "should_return_ok_with_tasks_for_specific_date",
queryParams: "?date=2026-01-20",
setupMock: func(m *mockTaskService) {
m.getTodayTasksFunc = func(ctx context.Context, userID uuid.UUID, date string) ([]TodayTaskResponse, error) {
assert.Equal(t, "2026-01-20", date) // Verify date was parsed correctly
return []TodayTaskResponse{{ID: testTaskID}}, nil
}
},
}Summary
This testing pattern combines:
- Table-driven tests for comprehensive scenario coverage
- Function field mocks for flexible dependency injection
- First-class functions for dynamic behavior configuration
- Higher-order functions for test setup composition
The result is a maintainable, readable, and comprehensive test suite that catches regressions early while remaining easy to extend and modify.