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

PatternImplementationProsConsUse When
Function Fieldsm.createRecordFunc = func(...) {...}✅ Simple
✅ No dependencies
✅ Flexible
✅ Type-safe
❌ More boilerplate
❌ Manual implementation
Small-medium projects, prefer simplicity
gomockEXPECT().CreateRecord(gomock.Any())✅ Auto-generated
✅ Powerful assertions
✅ Call ordering
❌ External dependency
❌ Complex setup
❌ Generated code
Large projects, strict call verification
testify/mockmock.On("CreateRecord").Return(...)✅ Popular
✅ Feature-rich
✅ Assertion helpers
❌ Runtime reflection
❌ Magic strings
❌ Not type-safe
When using testify suite
Hard-codedFixed return values✅ Very simple❌ Not flexible
❌ Can’t vary per test
Simple scenarios only

HTTP Handler Testing Pattern

Components

  1. httptest.ResponseRecorder: Captures HTTP response
  2. httptest.NewRequest: Creates test HTTP request
  3. Gin test router: Simulates routing without real server
  4. 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_successfully
  • should_return_unauthorized_when_user_id_not_in_context
  • should_return_conflict_when_duplicate_record

Best Practices

✅ Do’s

  1. Test all error paths: Success, validation, auth, business logic, infrastructure
  2. Use descriptive names: Make test failures self-explanatory
  3. Keep tests independent: No shared state between test cases
  4. Validate responses thoroughly: Check status code AND response body
  5. Use table-driven tests: When testing multiple scenarios for same handler

❌ Don’ts

  1. Don’t share mocks: Each test case should configure its own mock
  2. Don’t skip edge cases: Test empty arrays, nil values, invalid UUIDs
  3. Don’t test implementation details: Test behavior, not internals
  4. 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.