--- paths: - "**/*.rs" --- # Rust Testing > This file extends [common/testing.md](../common/testing.md) with Rust-specific content. ## Test Framework - **`#[test]`** with `#[cfg(test)]` modules for unit tests - **rstest** for parameterized tests and fixtures - **proptest** for property-based testing - **mockall** for trait-based mocking - **`#[tokio::test]`** for async tests ## Test Organization ```text my_crate/ ├── src/ │ ├── lib.rs # Unit tests in #[cfg(test)] modules │ ├── auth/ │ │ └── mod.rs # #[cfg(test)] mod tests { ... } │ └── orders/ │ └── service.rs # #[cfg(test)] mod tests { ... } ├── tests/ # Integration tests (each file = separate binary) │ ├── api_test.rs │ ├── db_test.rs │ └── common/ # Shared test utilities │ └── mod.rs └── benches/ # Criterion benchmarks └── benchmark.rs ``` Unit tests go inside `#[cfg(test)]` modules in the same file. Integration tests go in `tests/`. ## Unit Test Pattern ```rust #[cfg(test)] mod tests { use super::*; #[test] fn creates_user_with_valid_email() { let user = User::new("Alice", "alice@example.com").unwrap(); assert_eq!(user.name, "Alice"); } #[test] fn rejects_invalid_email() { let result = User::new("Bob", "not-an-email"); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("invalid email")); } } ``` ## Parameterized Tests ```rust use rstest::rstest; #[rstest] #[case("hello", 5)] #[case("", 0)] #[case("rust", 4)] fn test_string_length(#[case] input: &str, #[case] expected: usize) { assert_eq!(input.len(), expected); } ``` ## Async Tests ```rust #[tokio::test] async fn fetches_data_successfully() { let client = TestClient::new().await; let result = client.get("/data").await; assert!(result.is_ok()); } ``` ## Mocking with mockall Define traits in production code; generate mocks in test modules: ```rust // Production trait — pub so integration tests can import it pub trait UserRepository { fn find_by_id(&self, id: u64) -> Option; } #[cfg(test)] mod tests { use super::*; use mockall::predicate::eq; mockall::mock! { pub Repo {} impl UserRepository for Repo { fn find_by_id(&self, id: u64) -> Option; } } #[test] fn service_returns_user_when_found() { let mut mock = MockRepo::new(); mock.expect_find_by_id() .with(eq(42)) .times(1) .returning(|_| Some(User { id: 42, name: "Alice".into() })); let service = UserService::new(Box::new(mock)); let user = service.get_user(42).unwrap(); assert_eq!(user.name, "Alice"); } } ``` ## Test Naming Use descriptive names that explain the scenario: - `creates_user_with_valid_email()` - `rejects_order_when_insufficient_stock()` - `returns_none_when_not_found()` ## Coverage - Target 80%+ line coverage - Use **cargo-llvm-cov** for coverage reporting - Focus on business logic — exclude generated code and FFI bindings ```bash cargo llvm-cov # Summary cargo llvm-cov --html # HTML report cargo llvm-cov --fail-under-lines 80 # Fail if below threshold ``` ## Testing Commands ```bash cargo test # Run all tests cargo test -- --nocapture # Show println output cargo test test_name # Run tests matching pattern cargo test --lib # Unit tests only cargo test --test api_test # Specific integration test (tests/api_test.rs) cargo test --doc # Doc tests only ``` ## References See skill: `rust-testing` for comprehensive testing patterns including property-based testing, fixtures, and benchmarking with Criterion.