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

325 lines
7.6 KiB
Markdown

# 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:**
```bash
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:**
```bash
# Using cargo-watch (install with: cargo install cargo-watch)
cargo watch -x test # Re-run tests on file changes
```
**Coverage:**
```bash
# 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:**
```rust
#[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:**
```rust
// 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:**
```rust
#[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:**
```rust
#[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:**
```bash
# 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:**
```rust
/// 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):**
```rust
#[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:**
```rust
#[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`:
```rust
#[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:**
```rust
// 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*