Shane A. Stillwell

Go Test Helper

Let me explain the considerations for using t.Helper() with mock setup functions.

Generally, you should use t.Helper() in mock setup functions if they perform assertions or could fail in a way that needs to be reported in tests. Here’s a detailed breakdown with examples:

  1. Mock setup with potential failures - Should use t.Helper():
func setupMockDB(t *testing.T) *sql.DB {
    t.Helper()
    db, err := sql.Open("mock", "mock_connection")
    if err != nil {
        t.Fatalf("failed to setup mock DB: %v", err)
    }
    return db
}
  1. Mock setup with assertions - Should use t.Helper():
func setupUserServiceMock(t *testing.T) *mockUserService {
    t.Helper()
    mock := &mockUserService{}
    if err := mock.Validate(); err != nil {
        t.Fatalf("invalid mock configuration: %v", err)
    }
    return mock
}
  1. Simple mock creation without failures - No t.Helper() needed:
func newMockRepository() *mockRepository {
    return &mockRepository{
        users: make(map[string]User),
    }
}

Here’s a more comprehensive example showing both cases:

type MockHTTPClient struct {
    responses map[string]*http.Response
    calls     []string
}

// No t.Helper() needed - pure creation without testing logic
func NewMockHTTPClient() *MockHTTPClient {
    return &MockHTTPClient{
        responses: make(map[string]*http.Response),
        calls:     make([]string, 0),
    }
}

// Uses t.Helper() because it contains assertions and can fail
func setupMockHTTPClientWithResponses(t *testing.T, responses map[string]string) *MockHTTPClient {
    t.Helper()
    client := NewMockHTTPClient()
    
    for path, body := range responses {
        resp := &http.Response{
            Body: io.NopCloser(strings.NewReader(body)),
        }
        if body == "" {
            t.Fatalf("empty response body for path: %s", path)
        }
        client.responses[path] = resp
    }
    
    return client
}

func TestUserService(t *testing.T) {
    // Simple mock - no helper needed
    mockRepo := newMockRepository()
    
    // Complex mock with potential failures - uses helper
    mockHTTP := setupMockHTTPClientWithResponses(t, map[string]string{
        "/users": `{"users": []}`,
        "/auth":  `{"token": "123"}`,
    })
    
    service := NewUserService(mockRepo, mockHTTP)
    // ... rest of the test
}

Here’s an example with a more complex mock that shows when to use and not use t.Helper():

type MockEmailSender struct {
    mu      sync.Mutex
    sent    []Email
    failing bool
}

// No t.Helper() needed - simple creation
func NewMockEmailSender() *MockEmailSender {
    return &MockEmailSender{
        sent: make([]Email, 0),
    }
}

// Uses t.Helper() because it sets up test conditions and validates
func setupEmailSenderWithFailure(t *testing.T, shouldFail bool) *MockEmailSender {
    t.Helper()
    sender := NewMockEmailSender()
    sender.failing = shouldFail
    
    // Verify initial state
    if len(sender.sent) != 0 {
        t.Fatal("new mock email sender should start with empty sent slice")
    }
    
    return sender
}

// Helper function for assertions - uses t.Helper()
func assertEmailSent(t *testing.T, sender *MockEmailSender, expected Email) {
    t.Helper()
    sender.mu.Lock()
    defer sender.mu.Unlock()
    
    for _, email := range sender.sent {
        if email.To == expected.To && email.Subject == expected.Subject {
            return
        }
    }
    t.Errorf("expected email not sent: %+v", expected)
}

func TestEmailNotification(t *testing.T) {
    // Simple mock creation - no helper needed
    sender := NewMockEmailSender()
    
    // Setup with test conditions - uses helper
    failingSender := setupEmailSenderWithFailure(t, true)
    
    // Test with normal sender
    notification := NewNotificationService(sender)
    err := notification.SendWelcomeEmail("[email protected]")
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
    
    // Assert using helper
    assertEmailSent(t, sender, Email{
        To:      "[email protected]",
        Subject: "Welcome!",
    })
    
    // Test with failing sender
    failingNotification := NewNotificationService(failingSender)
    if err := failingNotification.SendWelcomeEmail("[email protected]"); err == nil {
        t.Error("expected error from failing sender")
    }
}

Guidelines for when to use t.Helper() in mock-related functions:

  1. Use t.Helper() when the function:
  • Takes a *testing.T parameter
  • Performs assertions
  • Can fail and report test errors
  • Sets up complex test conditions that need validation
  • Configures mocks with test-specific behavior
  1. Don’t use t.Helper() when the function:
  • Simply creates a new mock instance
  • Doesn’t take a *testing.T parameter
  • Doesn’t perform any assertions or error reporting
  • Is used outside of tests
  • Only sets up basic state without validation

Remember that t.Helper() is about improving test failure reporting, so use it when the function’s failure should be attributed to the calling test rather than the helper function itself.