Files
bbs-md/.planning/codebase/TESTING.md
T
2026-02-28 20:12:08 +01:00

7.6 KiB

Testing Patterns

Analysis Date: 2026-02-28

Test Framework

Test Runner:

  • Rust built-in test framework (via cargo test)
  • No external test framework currently integrated
  • Can add tokio for async testing if needed later

Assertion Library:

  • Standard assert!() and assert_eq!() macros from standard library
  • Can add pretty_assertions crate for better failure output if desired

Run Commands:

cargo test              # Run all tests
cargo test -- --nocapture  # Run tests with output visible
cargo test -- --test-threads=1  # Run tests sequentially
cargo test --lib       # Run only library tests
cargo test --doc       # Run doctests

Watch Mode:

# Using cargo-watch (install with: cargo install cargo-watch)
cargo watch -x test    # Re-run tests on file changes

Coverage:

# Using cargo-tarpaulin (install with: cargo install cargo-tarpaulin)
cargo tarpaulin --out Html  # Generate HTML coverage report

Test File Organization

Location - Unit Tests:

  • Co-located with implementation: Tests in same file as source code
  • Tests go in submodule at end of source file

Location - Integration Tests:

  • Separate tests/ directory at workspace root
  • File: tests/integration_test.rs or similar
  • Each file is compiled as separate crate

Naming:

  • Unit test modules: #[cfg(test)] mod tests
  • Integration test files: integration_test.rs, e2e_test.rs
  • Test functions: test_function_name_description()
  • Unit tests: tests::test_parses_valid_input()

Current Structure:

src/
├── main.rs          # Contains #[cfg(test)] mod tests
├── lib.rs           # If created, contains library tests
└── ...

tests/
├── integration_test.rs
└── ...

Test Structure

Suite Organization - Unit Test Pattern:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_function_does_something() {
        let input = setup_test_data();
        let result = function_under_test(input);
        assert_eq!(result, expected_value);
    }
}

Suite Organization - Integration Test Pattern:

// tests/integration_test.rs
#[test]
fn test_system_integrates_correctly() {
    let app = App::new();
    let result = app.process_command("help");
    assert!(result.is_ok());
}

Patterns:

  • Setup: Create test data or fixtures in test function body
  • Teardown: Not typically needed in Rust; cleanup happens automatically when test scope ends
  • Assertion: Use assert!(), assert_eq!(), assert_ne!() for conditions

Mocking

Framework: No explicit mocking framework currently integrated

If mocking becomes necessary:

  • Use mockito crate for HTTP mocking
  • Use mockall crate for trait-based mocking
  • Use proptest crate for property-based testing

Current Approach:

  • Manual test doubles: Create simplified implementations of traits for testing
  • Dependency injection: Pass different implementations at test time
  • Stub data: Return hardcoded test data instead of external calls

Example of Manual Test Double:

#[cfg(test)]
mod tests {
    struct MockDataStore {
        data: Vec<String>,
    }

    impl DataStore for MockDataStore {
        fn fetch(&self, id: u32) -> Option<String> {
            self.data.get(id as usize).cloned()
        }
    }

    #[test]
    fn test_with_mock() {
        let mock = MockDataStore { data: vec!["test".to_string()] };
        let result = process_data(&mock);
        assert!(result.is_ok());
    }
}

What to Mock:

  • External service calls (network, file I/O)
  • Database operations
  • Time-dependent behavior
  • Hardware-dependent code

What NOT to Mock:

  • Core business logic
  • Standard library functions
  • Tested utility functions
  • Pure functions

Fixtures and Factories

Test Data:

  • Currently: Manual test data created in test functions
  • Pattern: Helper functions to build test objects

Example:

#[cfg(test)]
mod tests {
    fn create_test_user() -> User {
        User {
            id: 1,
            name: "Test User".to_string(),
            email: "test@example.com".to_string(),
        }
    }

    #[test]
    fn test_user_creation() {
        let user = create_test_user();
        assert_eq!(user.name, "Test User");
    }
}

Location:

  • Helper functions in same module as tests: #[cfg(test)] mod tests
  • If many fixtures needed: Create src/test_utils.rs (not included in release build via #[cfg(test)])

Coverage

Requirements: Not currently enforced

To Enable Coverage Tracking:

# Using llvm-cov (install with: cargo install cargo-llvm-cov)
cargo llvm-cov          # Generate coverage report
cargo llvm-cov --html   # HTML report in target/llvm-cov/html

Target for new code:

  • Aim for 80%+ coverage on public APIs
  • 100% coverage on critical paths
  • Document uncovered code with #[cfg(test)] or comments

Test Types

Unit Tests:

  • Scope: Single function or small module in isolation
  • Approach: Test behavior with various inputs including edge cases
  • Location: Same file as implementation in #[cfg(test)] mod tests
  • Tools: Standard library assertions

Integration Tests:

  • Scope: Multiple components working together
  • Approach: Test workflows and interactions between modules
  • Location: tests/ directory at workspace root
  • Tools: Standard library assertions

Doc Tests:

  • Scope: Examples in /// doc comments
  • Approach: Verify example code actually works
  • Location: Documentation strings in source
  • Tools: Built-in cargo test --doc

E2E Tests:

  • Currently: Not applicable (TUI application, no external API)
  • When applicable: Test complete user workflows end-to-end

Example Doc Test:

/// Calculate the sum of two numbers.
///
/// # Examples
///
/// ```
/// assert_eq!(add(2, 2), 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Common Patterns

Async Testing:

  • Not currently used (no async in codebase)
  • When needed: Use #[tokio::test] attribute with tokio crate

Example (future reference):

#[tokio::test]
async fn test_async_function() {
    let result = async_operation().await;
    assert!(result.is_ok());
}

Error Testing:

  • Test both success and failure paths
  • Use .is_err() or .is_ok() to test Result types
  • Use .is_some() or .is_none() to test Option types

Example:

#[test]
fn test_handles_invalid_input() {
    let result = parse_config("invalid");
    assert!(result.is_err());
}

#[test]
fn test_handles_valid_input() {
    let result = parse_config("valid");
    assert!(result.is_ok());
}

Property-Based Testing (future): If using proptest:

#[test]
fn test_reverse_is_involutive(input in ".*") {
    let reversed = reverse(&input);
    let double_reversed = reverse(&reversed);
    assert_eq!(input, double_reversed);
}

Test Organization Best Practices

File Structure:

src/
├── main.rs
├── ui.rs          # Contains #[cfg(test)] mod tests at bottom
├── handler.rs     # Contains #[cfg(test)] mod tests at bottom
└── model.rs       # Contains #[cfg(test)] mod tests at bottom

tests/
├── integration_tests.rs   # Full-system integration tests
└── fixtures/              # Test data files if needed

Test Module Template:

// At end of source file
#[cfg(test)]
mod tests {
    use super::*;

    // Setup helpers
    fn create_test_fixture() -> TestData {
        // ...
    }

    // Tests organized by function
    mod function_name_tests {
        use super::*;

        #[test]
        fn test_success_case() {
            // ...
        }

        #[test]
        fn test_error_case() {
            // ...
        }
    }
}

Testing analysis: 2026-02-28