๐งช Testing Guide
Comprehensive testing is crucial for ensuring the reliability and correctness of our financial application. This guide covers our testing strategy, tools, and best practices.
๐ฏ Testing Strategy
Unit Tests
- Test individual functions and methods in isolation
- Located alongside the code they test (e.g.,
_test.go
files) - Focus on pure business logic and domain rules
Integration Tests
- Test interactions between components
- Use in-memory or test database instances
- Verify event publishing/subscribing behavior
End-to-End Tests
- Test complete user flows
- Use test containers for external services
- Verify system behavior from API to database
๐ ๏ธ Test Suite Execution
Run All Tests
Run Tests with Race Detector
Run Tests in a Specific Package
๐ Code Coverage
Generate Coverage Report
This will:
- Run all tests with coverage
- Generate an HTML report at
docs/coverage.html
- Show coverage percentage in the terminal
๐ Event-Driven Testing
Testing Event Handlers
func TestDepositHandler(t *testing.T) {
// Setup test dependencies
bus := NewInMemoryEventBus()
repo := NewMockAccountRepository()
handler := NewDepositHandler(repo, bus)
// Register handler
bus.Subscribe("Deposit.Requested", handler.Handle)
// Publish test event
event := DepositRequestedEvent{
Amount: 100,
AccountID: "acc123",
}
bus.Emit(context.Background(), event)
// Verify state changes
acc, _ := repo.FindByID("acc123")
assert.Equal(t, 100, acc.Balance)
}
Testing Event Flows
func TestDepositFlow(t *testing.T) {
// Setup test environment
container := testutils.NewTestContainer(t)
defer container.Cleanup()
// Execute API request
resp, err := http.Post(
container.Server.URL + "/deposit",
"application/json",
strings.NewReader(`{"amount": 100, "account_id": "acc123"}`),
)
require.NoError(t, err)
require.Equal(t, http.StatusAccepted, resp.StatusCode)
// Simulate webhook callback
webhookResp, err := http.Post(
container.Server.URL + "/webhooks/payment",
"application/json",
strings.NewReader(`{"event": "payment.completed", "amount": 100, "account_id": "acc123"}`),
)
require.NoError(t, err)
require.Equal(t, http.StatusOK, webhookResp.StatusCode)
// Verify final state
acc, err := container.AccountRepo.FindByID("acc123")
require.NoError(t, err)
assert.Equal(t, 100, acc.Balance)
}
๐งช Test Doubles
Mocks
Use Mockery to generate mocks for interfaces:
Test Containers
Use test containers for integration testing:
func TestMain(m *testing.M) {
container, err := testcontainers.StartPostgresContainer()
if err != nil {
log.Fatal(err)
}
defer container.Terminate()
os.Exit(m.Run())
}
๐ Test Data Management
Fixtures
func createTestAccount(t *testing.T, repo AccountRepository) *Account {
acc := &Account{
ID: "test-account",
Balance: 1000,
Status: "active",
}
err := repo.Save(acc)
require.NoError(t, err)
return acc
}
Test Helpers
func mustParseTime(t *testing.T, value string) time.Time {
tm, err := time.Parse(time.RFC3339, value)
require.NoError(t, err)
return tm
}
๐ Continuous Integration
Our CI pipeline runs:
- Unit tests with race detection
- Integration tests with test containers
- Linting and static analysis
- Code coverage reporting
๐ Best Practices
- Isolate Tests: Each test should be independent
- Use Table Tests: For testing multiple scenarios
- Test Edge Cases: Zero values, nil checks, error conditions
- Benchmark Critical Paths: Use Go's built-in benchmarking
- Keep Tests Fast: Use mocks for slow dependencies
- Test Error Cases: Ensure proper error handling
- Verify State and Behavior: Check both state changes and interactions