TDD — Rust
Rust-specific TDD conventions for PurePoint's Rust codebase (pu-core, pu-daemon, pu-cli, pu-proto).
Test Framework
- Built-in
#[cfg(test)]module with#[test]attribute cargo testas the test runnercargo test -p pu-corefor per-crate testingcargo test -- --nocapturefor output visibility during debugging
Test Organization
- Unit tests: In
#[cfg(test)] mod tests {}at the bottom of each source file - Integration tests: In
tests/directory at crate root - Doc tests: In
///doc comments (use sparingly — only for public API examples)
Assert Format (Rust Adaptation)
Rust doesn't have the { given, should, actual, expected } assert function. Adapt the principle:
#[test]
fn given_new_user_should_create_account() {
// given
let credentials = Credentials::new("user@test.com", "password123");
// when
let result = create_account(&credentials);
// then
assert_eq!(result.unwrap().email, "user@test.com");
}
Rules:
- Name test functions as
given_{situation}_should_{behavior} - Use
// given,// when,// thencomments for structure - Prefer
assert_eq!andassert_ne!over bareassert! - Use
assert!(matches!(value, Pattern))for enum variants
Mocking Strategy
- Prefer trait-based dependency injection over mocking frameworks
- Define traits for external dependencies (daemon client, git, filesystem, SQLite)
- Implement test doubles as simple structs implementing the trait
- Use
mockallcrate only when trait-based DI is impractical - For integration tests, use real dependencies (actual SQLite, actual git repos in temp dirs)
Test Isolation
- Use
tempdir(viatempfilecrate) for filesystem tests - Each test creates its own SQLite database (in-memory or temp file)
- No shared global state — no
lazy_statictest fixtures - Use
#[serial]fromserial_testcrate only when testing global resources (e.g., daemon socket)
Async Tests
- Use
#[tokio::test]for async test functions - Prefer
#[tokio::test(flavor = "current_thread")]for deterministic behavior - Use
tokio::time::pause()for timer-dependent tests