Testing Strategy and Quality Assurance Design
July 20, 2025 · View on GitHub
Overview
This document outlines the comprehensive testing strategy for the NTP Pool monitoring system, with particular focus on the selector package's complex constraint validation and monitor selection algorithms.
Testing Philosophy
Layered Testing Approach
- Unit Tests: Isolated component testing with mocked dependencies
- Integration Tests: Cross-component testing with real database
- System Tests: End-to-end workflow validation
- Performance Tests: Load and benchmark testing
- Regression Tests: Prevent re-introduction of known issues
Test-Driven Quality Gates
- Coverage Targets: 80%+ overall, 95%+ for critical functions
- Safety Logic: 100% coverage requirement for all emergency/safety paths
- Critical Functions: Database integration, constraint checking, state transitions
- Performance: No regression > 10% for typical workloads
Selector Package Testing Strategy
Critical Test Areas
Constraint Validation Testing
Priority: High - Forms the core of monitor selection logic
Test Scenarios:
func TestNetworkConstraints(t *testing.T) {
tests := []struct {
name string
monitorIP string
serverIP string
shouldPass bool
}{
{"same_ipv4_subnet", "192.168.1.10", "192.168.1.20", false},
{"different_ipv4_subnet", "192.168.1.10", "192.168.2.20", true},
{"same_ipv6_subnet", "2001:db8:1::10", "2001:db8:1::20", false},
{"different_ipv6_subnet", "2001:db8:1::10", "2001:db8:2::20", true},
}
}
func TestAccountConstraints(t *testing.T) {
tests := []struct {
name string
monitorAccount uint32
serverAccount uint32
currentCount int
limit int
shouldPass bool
}{
{"same_account", 1, 1, 0, 2, false},
{"different_account_under_limit", 1, 2, 1, 2, true},
{"different_account_at_limit", 1, 2, 2, 2, false},
}
}
State Machine Testing
Priority: High - Validates monitor lifecycle transitions
Test Categories:
- Valid Transitions: All legal state changes
- Invalid Transitions: Blocked transitions with proper error handling
- Edge Cases: Bootstrap scenarios, emergency conditions
- Consistency: Global vs server state synchronization
func TestStateMachineTransitions(t *testing.T) {
tests := []struct {
name string
globalStatus MonitorStatus
serverStatus ServerScoreStatus
constraints []ConstraintViolation
expected CandidateState
}{
{"pending_active_monitor", StatusPending, StatusActive, nil, CandidateOut},
{"paused_monitor", StatusPaused, StatusTesting, nil, CandidateBlock},
{"healthy_testing", StatusActive, StatusTesting, nil, CandidateIn},
}
}
Emergency Override Testing
Priority: Critical - Safety mechanism testing
Test Scenarios:
- Zero active monitors with constraint violations
- Emergency promotion despite constraints
- Emergency conditions vs capacity limits
- Bootstrap scenarios with multiple constraints
func TestEmergencyOverride(t *testing.T) {
// Setup: Server with 0 active monitors, all candidates have constraint violations
server := createTestServer()
candidates := createCandidatesWithConstraintViolations()
// Expected: Emergency override allows promotion despite constraints
result := selector.ProcessServer(server.ID)
assert.Greater(t, result.ActiveMonitors, 0)
assert.Contains(t, result.Changes, "emergency override")
}
Test Data Management
Mathematical Test Validation
Ensure test conditions make expected outcomes possible:
// Example: Account limit testing
// MaxPerServer=2, ActiveCount=1, TestingCount=2
// Total limit = MaxPerServer + 1 = 3
// Current total = 1 + 2 = 3 (at limit)
// Promoting testing→active: would become 2 active + 1 testing = 3 (still valid)
accountLimits := map[uint32]*accountLimit{
1: {AccountID: 1, MaxPerServer: 2, ActiveCount: 1, TestingCount: 2},
}
Test Helper Functions
Centralized test data creation for consistency:
// Monitor creation helpers
func createHealthyMonitors(count int, status ServerScoreStatus) []evaluatedMonitor
func createMonitorsWithConstraintViolations(violationType ConstraintType) []evaluatedMonitor
func createAccountLimitsScenario(limits map[uint32]int) map[uint32]*accountLimit
// Validation helpers
func validateWorkingCounts(result RuleResult, expected workingCounts) error
func validateConstraintViolations(changes []statusChange, expected []violationType) error
func validateStateTransitions(changes []statusChange, expected []transition) error
Integration Testing Framework
Database Integration Testing
Scope: Real database operations with proper isolation
Infrastructure:
- Use CI tools (
./scripts/test-db.sh start) for database setup - Port allocation (3308: all test databases - unified for consistency)
- Clean database state for each test run
- Transaction rollbacks for test isolation
Test Scenarios:
func TestDatabaseIntegration(t *testing.T) {
// Test actual database operations
db := setupTestDatabase(t)
defer cleanupTestDatabase(t, db)
// Test constraint tracking persistence
testConstraintViolationPersistence(t, db)
// Test status change application
testStatusChangeApplication(t, db)
// Test concurrent operations
testConcurrentSelectorRuns(t, db)
}
Multi-Server Testing
Scope: Behavior across multiple servers with shared monitor pool
Test Cases:
- Monitor reassignment between servers
- Constraint interactions across servers
- Account limit enforcement globally
- Network diversity across server assignments
Performance Testing Strategy
Benchmark Testing
Scope: Algorithm performance under various load conditions
func BenchmarkSelectorProcessing(b *testing.B) {
scenarios := []struct {
name string
monitorCount int
serverCount int
}{
{"small_scale", 50, 5},
{"medium_scale", 500, 20},
{"large_scale", 2000, 100},
}
for _, scenario := range scenarios {
b.Run(scenario.name, func(b *testing.B) {
testData := generateTestScenario(scenario.monitorCount, scenario.serverCount)
b.ResetTimer()
for i := 0; i < b.N; i++ {
processAllServers(testData)
}
})
}
}
Performance Regression Testing
- Baseline measurements for algorithm execution time
- Memory allocation tracking for large datasets
- Constraint checking overhead analysis
- Database query performance validation
Test Coverage Analysis
Coverage Targets by Component
Selector Core (process.go): 90%+
- Contains main selection algorithm
- All rule implementations
- Working count management
Constraints (constraints.go): 95%+
- Network constraint validation
- Account constraint checking
- Constraint type definitions
State Management (state.go): 95%+
- State determination logic
- Transition validation
- Emergency condition handling
Grandfathering (grandfathering.go): 85%+
- Grandfathering detection
- Historical violation tracking
Coverage Measurement Commands
# Generate coverage profile
go test -coverprofile=coverage.out ./selector
# View coverage by function
go tool cover -func=coverage.out | grep -E "(applyRule|calculateSafety|determineState)"
# Generate HTML coverage report
go tool cover -html=coverage.out -o coverage.html
# Enforce coverage thresholds in CI
go tool cover -func=coverage.out | tail -1 | awk '{print \$3}' | sed 's/%//' | awk '{if(\$1<80) exit 1}'
Debugging and Troubleshooting Strategy
Test Failure Analysis Framework
Step 1: Understand Data Dependencies
- Trace through test setup to identify required data relationships
- Verify foreign key relationships are satisfied
- Check monitor types and server compatibility
Step 2: Verify Query Requirements
- Use
Grepto find query definitions - Read actual SQL to understand JOIN conditions and WHERE clauses
- Ensure test data satisfies all query conditions
Step 3: Use CI Tools First
./scripts/test-ci-local.sh- Full CI environment emulation./scripts/test-scorer-integration.sh- Component-specific tests./scripts/diagnose-ci.sh- Comprehensive failure diagnostics
Common Bug Patterns and Tests
Safety Variable Scope Creep:
func TestSafetyVariableScopeCreep(t *testing.T) {
// Test that maxRemovals=0 only affects intended operations
// Verify constraint cleanup can proceed when safety limits active
}
Mathematical Consistency:
func TestWorkingCountConsistency(t *testing.T) {
// Verify working counts are updated after each change
// Test that promotion/demotion math remains consistent
}
Self-Reference Bugs:
func TestSelfExclusionInConstraints(t *testing.T) {
// Verify entities are excluded from conflict detection with themselves
}
Test Automation and CI Integration
Continuous Integration Requirements
Pre-commit Validation:
# CI Pipeline Integration
- name: Test Coverage Check
run: |
go test -coverprofile=coverage.out ./selector
COVERAGE=$(go tool cover -func=coverage.out | tail -1 | awk '{print \$3}' | sed 's/%//')
if [ $COVERAGE -lt 80 ]; then
echo "Coverage $COVERAGE% is below 80% threshold"
exit 1
fi
- name: Safety Logic Coverage
run: |
# Verify 100% coverage of emergency and safety functions
go test -coverprofile=safety.out -run="Emergency|Safety" ./selector
Integration Test Matrix:
- Multiple Go versions (1.19, 1.20, 1.21)
- Multiple database versions (MySQL 8.0, 8.1)
- Concurrent test execution with race detection
Test Environment Management
Database Port Allocation:
- 3308: All test databases (unified port for consistency)
Test Data Isolation:
- Use separate databases per test suite
- Implement test-specific prefixes for data separation
- Clean shutdown procedures to prevent test interference
Quality Metrics and Monitoring
Test Quality Indicators
Coverage Metrics:
- Line coverage percentage by package
- Function coverage for critical paths
- Branch coverage for conditional logic
Test Effectiveness:
- Bug detection rate in testing vs production
- Test execution time trends
- Flaky test identification and remediation
Regression Prevention:
- Tests per bug report ratio
- Post-incident test additions
- Test coverage delta on code changes
Operational Test Monitoring
Test Execution Tracking:
- CI test success/failure rates
- Test execution duration trends
- Resource usage during test runs
Test Quality Trends:
- Coverage improvement over time
- Test addition rate vs code growth
- Critical path test stability
Test Maintenance Strategy
Regular Review Process
Monthly Coverage Review:
- Identify coverage gaps in new code
- Review test effectiveness for recent bug fixes
- Update test scenarios for changed requirements
Quarterly Test Architecture Review:
- Assess test execution performance
- Identify opportunities for test consolidation
- Review mock usage and integration boundaries
Post-Incident Test Enhancement:
- Add tests for every production issue discovered
- Validate that new tests would have caught the issue
- Document test scenarios in incident retrospectives
Test Documentation Requirements
Test Scenario Documentation:
- Complex test cases include mathematical explanations
- Edge case documentation with business justification
- Integration test workflow documentation
Test Data Documentation:
- Explain relationships between test entities
- Document constraint calculations and expected outcomes
- Provide examples of valid and invalid test scenarios
This comprehensive testing strategy ensures robust validation of the complex selector algorithms while maintaining development velocity and preventing regressions.