Skip to main content

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:

  1. Unit Tests: Follow Rust's standard #[cfg(test)] pattern, co-located with the code they test
  2. Shared Test Configuration: Maintained in a dedicated src/tests/ directory
  3. Integration Tests: Leverage shared test configuration while remaining focused on specific functionality
  4. 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

  1. Maintainability: Test configuration is centralized and easy to update
  2. Consistency: Tests use the same constants and setup across the codebase
  3. Scalability: The approach scales well as the application grows
  4. Integration Focus: Enables proper testing of cross-component functionality
  5. Developer Experience: Balances convenience with structure
  6. Isolation: Mock implementations enable isolated testing of components
  7. 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.