# Testing in Rust ## Unit Tests ```rust // Tests in same file #[cfg(test)] mod tests { use super::*; #[test] fn test_addition() { assert_eq!(2 + 2, 4); } #[test] fn test_subtraction() { assert!(10 - 5 == 5); } #[test] #[should_panic(expected = "division by zero")] fn test_panic() { divide(10, 0); } #[test] fn test_result() -> Result<(), String> { let result = divide(10, 2)?; assert_eq!(result, 5); Ok(()) } #[test] #[ignore] fn expensive_test() { // Run with: cargo test -- --ignored } } // Assertions fn assert_examples() { assert!(true); assert_eq!(2 + 2, 4); assert_ne!(2 + 2, 5); // Custom messages assert!(value > 0, "Value must be positive, got {}", value); assert_eq!(result, expected, "Calculation failed"); } ``` ## Doctests ```rust /// Adds two numbers together. /// /// # Examples /// /// ``` /// use mylib::add; /// /// let result = add(2, 3); /// assert_eq!(result, 5); /// ``` /// /// ```should_panic /// use mylib::divide; /// /// divide(10, 0); // This will panic /// ``` /// /// ```ignore /// // This code won't compile but won't fail the test /// let x = undefined_function(); /// ``` pub fn add(a: i32, b: i32) -> i32 { a + b } ``` ## Integration Tests ```rust // tests/integration_test.rs use mylib; #[test] fn test_full_workflow() { let config = mylib::Config::new("test.conf"); let result = mylib::process(&config); assert!(result.is_ok()); } // tests/common/mod.rs - shared test utilities pub fn setup() -> TestContext { TestContext { db: create_test_db(), } } // tests/another_test.rs mod common; #[test] fn test_with_common() { let ctx = common::setup(); // Use ctx... } ``` ## Test Organization ```rust // Nested test modules #[cfg(test)] mod tests { use super::*; mod addition { use super::*; #[test] fn positive_numbers() { assert_eq!(add(2, 3), 5); } #[test] fn negative_numbers() { assert_eq!(add(-2, -3), -5); } } mod subtraction { use super::*; #[test] fn test_subtract() { assert_eq!(subtract(10, 5), 5); } } } ``` ## Test Fixtures and Setup ```rust struct TestContext { temp_dir: std::path::PathBuf, db: Database, } impl TestContext { fn setup() -> Self { let temp_dir = std::env::temp_dir().join("test"); std::fs::create_dir_all(&temp_dir).unwrap(); Self { temp_dir, db: Database::connect_test(), } } } impl Drop for TestContext { fn drop(&mut self) { // Cleanup std::fs::remove_dir_all(&self.temp_dir).ok(); self.db.disconnect(); } } #[test] fn test_with_fixture() { let ctx = TestContext::setup(); // Test uses ctx... // Automatic cleanup via Drop } ``` ## Async Tests ```rust use tokio; #[tokio::test] async fn test_async_function() { let result = async_operation().await; assert_eq!(result, 42); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_with_custom_runtime() { let result = concurrent_operation().await; assert!(result.is_ok()); } // Testing async with timeout #[tokio::test] async fn test_with_timeout() { let timeout = std::time::Duration::from_secs(5); let result = tokio::time::timeout(timeout, slow_operation()).await; assert!(result.is_ok()); } ``` ## Property-Based Testing (proptest) ```rust use proptest::prelude::*; // Simple property test proptest! { #[test] fn test_reversing_twice_is_identity(ref s in ".*") { let reversed: String = s.chars().rev().collect(); let double_reversed: String = reversed.chars().rev().collect(); assert_eq!(s, &double_reversed); } } // Custom strategies proptest! { #[test] fn test_addition_commutative(a in 0..1000i32, b in 0..1000i32) { assert_eq!(a + b, b + a); } #[test] fn test_vector_push_pop( ref v in prop::collection::vec(0..100i32, 0..100), item in 0..100i32 ) { let mut v = v.clone(); v.push(item); assert_eq!(v.pop(), Some(item)); } } // Complex custom strategies fn user_strategy() -> impl Strategy { (1..1000u64, "[a-z]{3,10}", "[a-z0-9.]+@[a-z]+\\.[a-z]+") .prop_map(|(id, name, email)| User { id, name, email }) } proptest! { #[test] fn test_user_serialization(user in user_strategy()) { let json = serde_json::to_string(&user).unwrap(); let deserialized: User = serde_json::from_str(&json).unwrap(); assert_eq!(user, deserialized); } } ``` ## Mocking ```rust // Using mockall use mockall::*; use mockall::predicate::*; #[automock] trait Database { fn get_user(&self, id: u64) -> Option; fn save_user(&mut self, user: User) -> Result<(), Error>; } #[test] fn test_with_mock() { let mut mock = MockDatabase::new(); mock.expect_get_user() .with(eq(1)) .times(1) .returning(|_| Some(User { id: 1, name: "Alice".to_string() })); mock.expect_save_user() .times(1) .returning(|_| Ok(())); // Use mock in test let user = mock.get_user(1); assert!(user.is_some()); } ``` ## Benchmarks (Criterion) ```rust // benches/my_benchmark.rs use criterion::{black_box, criterion_group, criterion_main, Criterion}; fn fibonacci(n: u64) -> u64 { match n { 0 => 1, 1 => 1, n => fibonacci(n - 1) + fibonacci(n - 2), } } fn criterion_benchmark(c: &mut Criterion) { c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20)))); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); // Cargo.toml: // [dev-dependencies] // criterion = "0.5" // // [[bench]] // name = "my_benchmark" // harness = false ``` ## Advanced Benchmarking ```rust use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; fn bench_multiple_sizes(c: &mut Criterion) { let mut group = c.benchmark_group("sorting"); for size in [10, 100, 1000, 10000].iter() { group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| { b.iter_batched( || generate_random_vec(size), |mut v| v.sort(), criterion::BatchSize::SmallInput, ); }); } group.finish(); } // Comparing implementations fn bench_comparison(c: &mut Criterion) { let mut group = c.benchmark_group("string_search"); group.bench_function("naive", |b| { b.iter(|| naive_search(black_box("haystack"), black_box("needle"))) }); group.bench_function("optimized", |b| { b.iter(|| optimized_search(black_box("haystack"), black_box("needle"))) }); group.finish(); } criterion_group!(benches, bench_multiple_sizes, bench_comparison); criterion_main!(benches); ``` ## Testing with External Resources ```rust // Testing file I/O #[test] fn test_file_operations() { use std::io::Write; let temp_dir = std::env::temp_dir(); let file_path = temp_dir.join("test_file.txt"); // Write let mut file = std::fs::File::create(&file_path).unwrap(); file.write_all(b"test content").unwrap(); // Read let content = std::fs::read_to_string(&file_path).unwrap(); assert_eq!(content, "test content"); // Cleanup std::fs::remove_file(&file_path).unwrap(); } // Testing with databases (using sqlx) #[sqlx::test] async fn test_database_operations(pool: sqlx::PgPool) -> sqlx::Result<()> { sqlx::query("INSERT INTO users (name) VALUES ($1)") .bind("Alice") .execute(&pool) .await?; let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users") .fetch_one(&pool) .await?; assert_eq!(count.0, 1); Ok(()) } ``` ## Snapshot Testing ```rust // Using insta crate use insta::assert_snapshot; #[test] fn test_output_format() { let data = generate_complex_output(); assert_snapshot!(data); } #[test] fn test_json_output() { let json = serde_json::to_string_pretty(&get_data()).unwrap(); assert_snapshot!(json); } // Run with: cargo insta test // Review snapshots: cargo insta review ``` ## Code Coverage ```rust // Using tarpaulin // cargo install cargo-tarpaulin // cargo tarpaulin --out Html --output-dir coverage // Using llvm-cov // cargo install cargo-llvm-cov // cargo llvm-cov --html ``` ## Fuzzing ```rust // Using cargo-fuzz // cargo install cargo-fuzz // cargo fuzz init // fuzz/fuzz_targets/fuzz_target_1.rs #![no_main] use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { let _ = mylib::parse_input(s); } }); // Run with: cargo fuzz run fuzz_target_1 ``` ## Best Practices - Write tests alongside production code in #[cfg(test)] modules - Use integration tests in tests/ directory for end-to-end testing - Include doctests in documentation for examples that must work - Use descriptive test names that explain what is being tested - Test edge cases (empty inputs, max values, etc.) - Use property-based testing for algorithmic code - Benchmark performance-critical code with criterion - Run tests in CI with cargo test --all-features - Use cargo test -- --nocapture to see println! output - Test error conditions with #[should_panic] or Result - Mock external dependencies for unit tests - Use test fixtures for complex setup/teardown - Run clippy on test code too - Measure code coverage and aim for high coverage - Use fuzzing for security-critical parsers - Test async code with tokio::test - Use snapshot testing for complex output validation