Testing Strategy - CAMT-CSV Project¶
Overview¶
This document defines the comprehensive testing strategy for the CAMT-CSV project, ensuring high code quality, reliability, and maintainability through systematic testing approaches.
Testing Philosophy¶
Core Principles¶
- Test Pyramid: More unit tests, fewer integration tests, minimal end-to-end tests
- Fast Feedback: Tests should run quickly to enable rapid development cycles
- Isolation: Tests should be independent and not affect each other
- Deterministic: Tests should produce consistent results across environments
- Maintainable: Tests should be easy to understand and modify
Quality Gates¶
- Minimum Coverage: 80% code coverage for all packages
- Critical Path Coverage: 100% coverage for parsing and validation logic
- Performance: All tests must complete within 30 seconds
- Reliability: Tests must pass consistently (>99% success rate)
Test Categories¶
1. Unit Tests¶
Purpose: Test individual functions and methods in isolation
Scope:
- Pure functions (data transformations, calculations)
- Business logic components
- Utility functions
- Error handling paths
Structure:
func TestParser_ParseTransaction(t *testing.T) {
tests := []struct {
name string
input string
expected models.Transaction
wantErr bool
}{
{
name: "valid transaction",
input: `<transaction>...</transaction>`,
expected: models.Transaction{
Date: "01.01.2024",
Amount: decimal.NewFromFloat(100.50),
},
wantErr: false,
},
{
name: "invalid XML",
input: `<invalid>`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParseTransaction(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expected.Date, result.Date)
assert.True(t, tt.expected.Amount.Equal(result.Amount))
})
}
}
Coverage Requirements:
- All public methods must have tests
- All error conditions must be tested
- Edge cases and boundary conditions must be covered
2. Integration Tests¶
Purpose: Test component interactions and external dependencies
Scope:
- Parser integration with file system
- Categorizer integration with AI services
- Configuration loading and validation
- CLI command execution
Test Structure:
func TestCAMTParser_Integration(t *testing.T) {
// IMPORTANT: Use t.TempDir() for all file system tests
// It provides automatic cleanup and isolation
tempDir := t.TempDir()
testFile := filepath.Join(tempDir, "test.xml")
outputFile := filepath.Join(tempDir, "output.csv")
// Create test data
err := os.WriteFile(testFile, []byte(validCAMTXML), 0644)
require.NoError(t, err)
// Execute integration (note: context.Context is first parameter)
parser := camtparser.New()
err = parser.ConvertToCSV(context.Background(), testFile, outputFile)
// Verify results
assert.NoError(t, err)
assert.FileExists(t, outputFile)
// Verify CSV content
content, err := os.ReadFile(outputFile)
require.NoError(t, err)
lines := strings.Split(string(content), "\n")
assert.Contains(t, lines[0], "Date,Description,Amount")
assert.Greater(t, len(lines), 1)
}
3. End-to-End Tests¶
Purpose: Test complete user workflows
Scope:
- CLI command execution
- File processing workflows
- Error handling and recovery
Test Structure:
func TestCLI_EndToEnd(t *testing.T) {
// Setup test environment
tempDir := t.TempDir()
inputFile := createTestCAMTFile(t, tempDir)
outputFile := filepath.Join(tempDir, "output.csv")
// Execute CLI command
cmd := exec.Command("camt-csv", "convert",
"--input", inputFile,
"--output", outputFile,
"--log-level", "debug")
output, err := cmd.CombinedOutput()
// Verify execution
assert.NoError(t, err, "CLI command failed: %s", output)
assert.FileExists(t, outputFile)
// Verify output quality
transactions, err := readCSVTransactions(outputFile)
assert.NoError(t, err)
assert.Greater(t, len(transactions), 0)
}
Testing Patterns¶
1. Table-Driven Tests¶
When to Use: Testing multiple scenarios with similar structure
func TestCategorizeTransaction(t *testing.T) {
tests := []struct {
name string
description string
partyName string
expected string
}{
{"grocery store", "MIGROS ZURICH", "MIGROS", "Groceries"},
{"transport", "SBB TICKET", "SBB", "Transportation"},
{"unknown", "UNKNOWN MERCHANT", "UNKNOWN", "Uncategorized"},
}
categorizer := NewCategorizer()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tx := models.CategorizeTransaction{
Description: tt.description,
PartyName: tt.partyName,
}
category, err := categorizer.CategorizeTransaction(tx)
assert.NoError(t, err)
assert.Equal(t, tt.expected, category.Name)
})
}
}
2. Mock Testing¶
When to Use: Testing with external dependencies
type MockAIService struct {
mock.Mock
}
func (m *MockAIService) CategorizeTransaction(ctx context.Context, tx models.CategorizeTransaction) (*models.Category, error) {
args := m.Called(ctx, tx)
return args.Get(0).(*models.Category), args.Error(1)
}
func TestCategorizer_WithAIFallback(t *testing.T) {
// Tests use dependency injection via NewCategorizer() with mock stores
mockStore := &MockCategoryStore{}
mockAI := new(MockAIService)
categorizer := NewCategorizer(mockStore, mockAI)
// TransactionCategorizer interface signature
category, err := categorizer.Categorize(context.Background(), "Unknown Transaction", false, "100", "01.01.2024", "")
assert.NoError(t, err)
assert.Equal(t, "AI_Category", category.Name)
}
3. Test Fixtures¶
Purpose: Reusable test data and setup
// testdata/fixtures.go
package testdata
var ValidCAMTXML = `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
<!-- Valid CAMT.053 content -->
</BkToCstmrStmt>
</Document>`
func CreateTempCAMTFile(t *testing.T, dir string) string {
file := filepath.Join(dir, "test.xml")
err := os.WriteFile(file, []byte(ValidCAMTXML), 0644)
require.NoError(t, err)
return file
}
Test Data Management¶
1. Sample Files¶
Location: samples/ directory
Purpose: Real-world test data for integration tests
samples/
├── camt/
│ ├── valid_statement.xml
│ ├── empty_statement.xml
│ └── malformed.xml
├── pdf/
│ ├── bank_statement.pdf
│ └── complex_layout.pdf
├── csv/
│ ├── revolut_sample.csv
│ └── selma_sample.csv
└── expected/
├── camt_expected.csv
└── revolut_expected.csv
2. Test Data Generation¶
func GenerateTestTransaction() models.Transaction {
return models.Transaction{
Date: "01.01.2024",
Amount: decimal.NewFromFloat(rand.Float64() * 1000),
Currency: "CHF",
CreditDebit: []string{"CRDT", "DBIT"}[rand.Intn(2)],
Description: fmt.Sprintf("Test Transaction %d", rand.Int()),
}
}
func GenerateTestTransactions(count int) []models.Transaction {
transactions := make([]models.Transaction, count)
for i := 0; i < count; i++ {
transactions[i] = GenerateTestTransaction()
}
return transactions
}
Performance Testing¶
1. Benchmarks¶
Purpose: Measure and track performance over time
func BenchmarkParser_ParseFile(b *testing.B) {
parser := camtparser.New()
testFile := "testdata/large_statement.xml"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := parser.ParseFile(testFile)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkCategorizer_CategorizeTransaction(b *testing.B) {
categorizer := NewCategorizer()
tx := models.CategorizeTransaction{
Description: "MIGROS ZURICH",
PartyName: "MIGROS",
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := categorizer.CategorizeTransaction(tx)
if err != nil {
b.Fatal(err)
}
}
}
2. Load Testing¶
Purpose: Test behavior under high load
func TestParser_LargeFile(t *testing.T) {
if testing.Short() {
t.Skip("Skipping large file test in short mode")
}
// Generate large test file (10MB+)
largeFile := generateLargeCAMTFile(t, 10000) // 10k transactions
parser := camtparser.New()
start := time.Now()
transactions, err := parser.ParseFile(largeFile)
duration := time.Since(start)
assert.NoError(t, err)
assert.Equal(t, 10000, len(transactions))
assert.Less(t, duration, 10*time.Second, "Large file parsing took too long")
}
Test Organization¶
1. Directory Structure¶
test/
├── unit/ # Unit tests (alongside source code)
├── integration/ # Integration tests
├── e2e/ # End-to-end tests
├── benchmarks/ # Performance benchmarks
├── testdata/ # Test data and fixtures
└── helpers/ # Test utilities and helpers
2. Test Naming Conventions¶
// Unit tests: Test<Type>_<Method>_<Scenario>
func TestCAMTParser_ParseFile_ValidInput(t *testing.T) {}
func TestCAMTParser_ParseFile_InvalidFormat(t *testing.T) {}
// Integration tests: TestIntegration_<Component>_<Scenario>
func TestIntegration_Parser_FileProcessing(t *testing.T) {}
// End-to-end tests: TestE2E_<Workflow>_<Scenario>
func TestE2E_ConvertCommand_Success(t *testing.T) {}
Test Execution¶
1. Local Development¶
# Using Makefile (recommended)
make test # Run all tests
make test-race # Run tests with race detector
make coverage # Generate HTML coverage report
make lint # Run golangci-lint
make security # Run gosec security scan
# Direct commands (for specific cases)
go test ./... # Run all tests
go test -cover ./... # Run tests with coverage
go test ./internal/camtparser # Run specific test package
go test -bench=. ./... # Run benchmarks
go test -race ./... # Run tests with race detection
go test -v -run TestFunctionName ./path/to/package # Single test
2. CI/CD Pipeline¶
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [1.21, 1.22]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- name: Run tests
run: |
go test -v -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
Test Quality Assurance¶
1. Test Review Checklist¶
- [ ] Tests are independent and can run in any order
- [ ] Tests have descriptive names explaining what they test
- [ ] Tests cover both happy path and error conditions
- [ ] Tests use appropriate assertions with helpful messages
- [ ] Tests clean up resources (temp files, connections)
- [ ] Tests run quickly (< 1 second per test)
2. Coverage Analysis¶
# Generate coverage report
go test -coverprofile=coverage.out ./...
# View coverage by function
go tool cover -func=coverage.out
# Generate HTML coverage report
go tool cover -html=coverage.out -o coverage.html
3. Test Maintenance¶
- Regular Review: Review and update tests when code changes
- Flaky Test Detection: Monitor test reliability and fix unstable tests
- Performance Monitoring: Track test execution time and optimize slow tests
- Dependency Updates: Keep test dependencies up to date
Continuous Improvement¶
1. Metrics Tracking¶
- Test execution time trends
- Code coverage trends
- Test failure rates
- Performance benchmark results
2. Test Automation¶
- Automatic test generation for new parsers
- Property-based testing for data transformations
- Mutation testing to verify test quality
3. Team Practices¶
- Test-Driven Development: Write tests before implementation
- Code Reviews: Include test review in code review process
- Knowledge Sharing: Regular sessions on testing best practices
- Tool Evaluation: Continuously evaluate new testing tools and techniques
This testing strategy ensures comprehensive coverage and high quality for the CAMT-CSV project while maintaining development velocity and reliability.