Testing Strategy¶
This document outlines the testing approach and guidelines for reviewtask.
Testing Philosophy¶
- Test real workflows over isolated units
- Mock external dependencies (GitHub API, Claude CLI)
- Use golden tests for output stability
- Maintain high coverage for critical paths
Test Types¶
1. Unit Tests¶
Located alongside source files as *_test.go
.
Purpose: Test individual functions and methods in isolation.
Example:
// internal/ai/simple_task_test.go
func TestSimpleTaskRequest_Structure(t *testing.T) {
task := SimpleTaskRequest{
Description: "Test description",
Priority: "high",
}
if task.Description != "Test description" {
t.Errorf("Expected description 'Test description', got %s", task.Description)
}
}
2. Golden Tests¶
Compare output against known-good snapshots.
Purpose: Detect unintended changes in output format.
Example:
// internal/ai/simple_prompt_golden_test.go
func TestBuildSimpleCommentPrompt_Golden(t *testing.T) {
got := analyzer.buildSimpleCommentPrompt(ctx)
goldenPath := "testdata/prompts/simple/english.golden"
if updateGoldenEnabled() {
writeGolden(t, goldenPath, got)
}
want := loadGolden(t, goldenPath)
if got != want {
t.Fatalf("Prompt mismatch")
}
}
Updating Golden Files:
3. Integration Tests¶
Test complete workflows end-to-end.
Purpose: Verify components work together correctly.
Location: test/
directory
Example:
// test/workflow_test.go
func TestCompleteWorkflow(t *testing.T) {
// Setup
client := setupMockGitHub()
ai := setupMockAI()
// Execute workflow
err := FetchAndAnalyze(123)
// Verify results
tasks := LoadTasks(123)
assert.Equal(t, 5, len(tasks))
}
4. Concurrent Tests¶
Test thread safety and race conditions.
Purpose: Ensure concurrent operations are safe.
Example:
// internal/storage/write_worker_test.go
func TestWriteWorker_ConcurrentWrites(t *testing.T) {
worker := NewWriteWorker(manager, 100, false)
var wg sync.WaitGroup
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
worker.QueueTask(task)
}()
}
wg.Wait()
// Verify all tasks written correctly
}
Run with Race Detector:
Test Organization¶
Directory Structure¶
reviewtask/
├── internal/
│ ├── ai/
│ │ ├── analyzer.go
│ │ ├── analyzer_test.go # Unit tests
│ │ ├── simple_task_test.go # Focused tests
│ │ ├── simple_prompt_golden_test.go # Golden tests
│ │ └── testdata/ # Test fixtures
│ │ └── prompts/
│ │ └── simple/
│ │ ├── english.golden
│ │ └── japanese.golden
│ └── storage/
│ ├── manager.go
│ ├── manager_test.go
│ └── write_worker_test.go
└── test/ # Integration tests
├── workflow_test.go
└── fixtures/
Test Naming Conventions¶
- Unit tests:
Test{FunctionName}_{Scenario}
- Golden tests:
Test{Function}_Golden
- Benchmarks:
Benchmark{Operation}
- Examples:
Example{Function}
Writing Tests¶
Test Structure¶
func TestFunctionName_Scenario(t *testing.T) {
// Arrange
input := setupTestData()
expected := expectedResult()
// Act
result := FunctionUnderTest(input)
// Assert
if result != expected {
t.Errorf("Expected %v, got %v", expected, result)
}
}
Table-Driven Tests¶
func TestExtractJSON(t *testing.T) {
testCases := []struct {
name string
input string
expected string
wantErr bool
}{
{
name: "Plain JSON",
input: `{"test": "value"}`,
expected: `{"test": "value"}`,
wantErr: false,
},
{
name: "Markdown wrapped",
input: "```json\n{\"test\": \"value\"}\n```",
expected: `{"test": "value"}`,
wantErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := ExtractJSON(tc.input)
if tc.wantErr && err == nil {
t.Error("Expected error but got none")
}
if result != tc.expected {
t.Errorf("Expected %s, got %s", tc.expected, result)
}
})
}
}
Mocking External Dependencies¶
Mock Claude Client¶
type MockClaudeClient struct {
ExecuteFunc func(input, format string) (string, error)
}
func (m *MockClaudeClient) Execute(input, format string) (string, error) {
if m.ExecuteFunc != nil {
return m.ExecuteFunc(input, format)
}
return "", nil
}
Mock GitHub Client¶
type MockGitHubClient struct {
PRs map[int]*github.PullRequest
Reviews map[int][]*github.Review
}
func (m *MockGitHubClient) GetPR(number int) (*github.PullRequest, error) {
if pr, ok := m.PRs[number]; ok {
return pr, nil
}
return nil, fmt.Errorf("PR not found")
}
Testing Error Cases¶
func TestHandleError(t *testing.T) {
testCases := []struct {
name string
setupMock func() *MockClient
expectedErr string
}{
{
name: "Network error",
setupMock: func() *MockClient {
return &MockClient{
ReturnError: errors.New("network timeout"),
}
},
expectedErr: "network timeout",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mock := tc.setupMock()
err := ProcessWithClient(mock)
if err == nil || !strings.Contains(err.Error(), tc.expectedErr) {
t.Errorf("Expected error containing %q, got %v", tc.expectedErr, err)
}
})
}
}
Test Coverage¶
Measuring Coverage¶
# Generate coverage report
go test -coverprofile=coverage.out ./...
# View coverage in terminal
go tool cover -func=coverage.out
# View coverage in browser
go tool cover -html=coverage.out
# Coverage for specific package
go test -cover ./internal/ai
Coverage Goals¶
- Overall: Aim for >80% coverage
- Critical paths: 95%+ coverage
- AI processing: 90%+ coverage
- Storage operations: 90%+ coverage
- Error handling: 100% coverage
Excluding from Coverage¶
// Some legitimate exclusions:
// - Panic recovery code
// - Unreachable defensive code
// - Generated code
// - Test helpers
Testing Commands¶
Running Tests¶
# All tests
go test ./...
# Specific package
go test ./internal/ai
# Verbose output
go test -v ./...
# Short tests only (skip slow tests)
go test -short ./...
# With timeout
go test -timeout 30s ./...
# Parallel execution
go test -parallel 4 ./...
Test Caching¶
Continuous Integration¶
GitHub Actions Workflow¶
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.21
- run: go test -v -race -cover ./...
Pre-commit Hooks¶
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: go-test
name: go test
entry: go test ./...
language: system
pass_filenames: false
Best Practices¶
1. Test Behavior, Not Implementation¶
❌ Bad:
func TestInternalState(t *testing.T) {
obj := NewObject()
if obj.internalField != 42 {
t.Error("Internal field not set")
}
}
✅ Good:
func TestObjectBehavior(t *testing.T) {
obj := NewObject()
result := obj.Process()
if result != expected {
t.Error("Unexpected behavior")
}
}
2. Use Descriptive Test Names¶
❌ Bad: TestProcess
✅ Good: TestProcess_WithEmptyInput_ReturnsError
3. Keep Tests Independent¶
Each test should: - Set up its own data - Clean up after itself - Not depend on test execution order
4. Use t.Helper() for Test Helpers¶
func assertTaskEqual(t *testing.T, expected, actual Task) {
t.Helper() // Reports errors at call site
if expected != actual {
t.Errorf("Tasks not equal\nExpected: %+v\nActual: %+v", expected, actual)
}
}
5. Test Edge Cases¶
Always test: - Empty inputs - Nil values - Maximum values - Concurrent access - Error conditions
Debugging Failed Tests¶
Verbose Output¶
Run Single Test¶
Debug with Print Statements¶
Use Delve Debugger¶
Performance Testing¶
Writing Benchmarks¶
func BenchmarkTaskGeneration(b *testing.B) {
comment := setupTestComment()
b.ResetTimer()
for i := 0; i < b.N; i++ {
GenerateTasks(comment)
}
}
Running Benchmarks¶
# Run benchmarks
go test -bench=. ./internal/ai
# With memory allocation stats
go test -bench=. -benchmem ./internal/ai
# Compare benchmarks
go test -bench=. -count=10 ./internal/ai | tee old.txt
# Make changes
go test -bench=. -count=10 ./internal/ai | tee new.txt
benchstat old.txt new.txt
Test Maintenance¶
Updating Tests¶
When changing functionality: 1. Update tests first (TDD approach) 2. Make code changes 3. Verify tests pass 4. Update golden files if needed 5. Update documentation
Removing Obsolete Tests¶
Periodically review and remove: - Tests for deleted features - Duplicate tests - Tests that no longer provide value
Test Documentation¶
Document complex tests: