Hybrid Testing Strategy for Runway Calculator
This document outlines our hybrid approach to testing in the Runway Calculator application, which combines the standard Rust testing pattern with additional organization for complex testing scenarios.
Testing Approach
Our testing strategy employs a hybrid approach that balances Rust's conventional testing patterns with the needs of a complex web application:
- Unit Tests: Follow Rust's standard
#[cfg(test)]pattern, co-located with the code they test - Shared Test Configuration: Maintained in a dedicated
src/tests/directory - Integration Tests: Leverage shared test configuration while remaining focused on specific functionality
- Mocking: Extensive use of mock implementations for testing HTTP requests, auth services, and components
Rationale
We've adopted this hybrid approach for several practical reasons:
1. Test Configuration Management
By isolating test configuration (database URLs, credentials, test constants) in dedicated files:
- We maintain a single source of truth for test settings
- We avoid duplicating test constants across multiple test modules
- We can easily adjust test parameters for different environments
2. Supporting Integration Tests
Our application includes complex integration points that benefit from shared setup:
- Authentication testing involves database access and test user management
- Financial calculations need consistent test scenarios across components
- UI components require simulated authentication contexts
3. Balancing Rust Conventions with Project Needs
This approach respects Rust's standard practices while addressing real-world testing needs:
- Simple unit tests remain close to the code they're testing
- Complex tests get the structure they need without duplicating setup code
- We maintain good test organization as the application grows
4. Realistic Enterprise Patterns
Our hybrid model reflects patterns seen in production applications:
- Authentication testing often requires more elaborate infrastructure
- Financial applications need consistent test data across boundaries
- Web applications need to test both frontend and backend components
5. Reusable Test Infrastructure
The shared test components enable:
- Consistent testing across different modules
- Easier maintenance of test data and configurations
- Straightforward adaptation to CI/CD pipeline requirements
Implementation
Our implementation follows these patterns:
In-File Unit Tests
// In source file (e.g., src/models/financial_event.rs)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_financial_event_creation() {
// Test implementation
}
}
Shared Test Configuration
// In src/tests/auth_test_config.rs
pub const TEST_DB_URL: &str = "http://localhost:8529";
pub const TEST_USER: &str = "test_user";
pub const TEST_PASSWORD: &str = "test_password";
Integration Tests Using Shared Configuration
// In source file with integration tests
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::auth_test_config::*;
#[test]
fn test_authenticated_scenario_creation() {
// Uses shared test constants
let client = connect_to_db(TEST_DB_URL);
let auth = authenticate(TEST_USER, TEST_PASSWORD);
// Test implementation
}
}
Mock Implementations for Component Testing
// Mock AuthService for testing login component
struct MockAuthService {
login_result: RefCell<Result<User, String>>,
login_calls: RefCell<Vec<LoginData>>,
}
impl MockAuthService {
fn new(login_result: Result<User, String>) -> Self {
Self {
login_result: RefCell::new(login_result),
login_calls: RefCell::new(Vec::new()),
}
}
async fn login(&self, login_data: LoginData) -> Result<User, String> {
self.login_calls.borrow_mut().push(login_data);
self.login_result.borrow().clone()
}
}
Benefits of This Approach
- Maintainability: Test configuration is centralized and easy to update
- Consistency: Tests use the same constants and setup across the codebase
- Scalability: The approach scales well as the application grows
- Integration Focus: Enables proper testing of cross-component functionality
- Developer Experience: Balances convenience with structure
- Isolation: Mock implementations enable isolated testing of components
- Verification: Tracking calls to mocked methods enables verification of interactions
Test Categories and Implementation
Our tests fall into these main categories:
1. Model Tests
These tests verify core data structures and calculations:
- Financial event creation and manipulation
- Scenario calculations
- Seasonality adjustments
2. Authentication Tests
Our authentication testing system includes:
-
HTTP Mocking: Mock HTTP client for testing API requests
- Request tracking for validation
- Configurable responses for success and error cases
- Network error simulation
-
Auth Service Mocking: Mock auth service for testing components
- Login success/failure handling
- Registration validation
- Permission checks
- Email verification workflow
-
Component Testing: Mock auth services for UI components
- Login form validation and submission
- Registration form validation
- Profile display with different user types
- Authentication page navigation
3. Component Tests
Tests for UI components use simplified mock implementations:
- Form validation testing
- Event handling verification
- Display logic for different states (loading, error, success)
- Navigation between components
4. Integration Tests
Integration tests verify cross-component functionality:
- Authentication flow from login to dashboard
- Scenario creation and modification workflow
- Event creation and impact on projections
Each category employs the appropriate level of test infrastructure while maintaining proximity to the code under test whenever possible.