# Rust Testing Patterns Patterns for writing tests in Rust, extracted from the standard library source. **Source:** [rust-lang/rust](https://github.com/rust-lang/rust) at commit [`f53b654`](https://github.com/rust-lang/rust/tree/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6) **Stats:** 4,136 `#[test]` functions, 338 `#[cfg(test)]` modules, 16,728 assert macros, 275+ `#[should_panic]` tests, 389 `#[track_caller]` annotations. --- ## 1. #[cfg(test)] Module Organization ### Source: [library/core/src/option.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/option.rs) 338 `#[cfg(test)]` modules in the stdlib. ```rust // Tests live in a cfg(test) module at the bottom of the file: #[cfg(test)] mod tests { use super::*; #[test] fn test_option_is_some() { let x: Option = Some(2); assert!(x.is_some()); } } ``` ### Why `#[cfg(test)]` means the module is only compiled when running tests. It's not included in the release binary. The module has full access to private items via `use super::*`. ### When to Use **Triggers:** - Unit tests for the same module (testing private functions) - Small, focused tests that don't need external resources - Tests that access private implementation details **Example — before:** ```rust // Tests in a separate file can't access private functions // tests/my_module.rs use my_crate::public_function; // can't test `private_helper` directly ``` **Example — after:** ```rust // src/my_module.rs fn private_helper(x: i32) -> i32 { x * 2 } pub fn public_function(x: i32) -> i32 { private_helper(x) + 1 } #[cfg(test)] mod tests { use super::*; #[test] fn helper_doubles() { assert_eq!(private_helper(5), 10); // can test private fn } #[test] fn public_uses_helper() { assert_eq!(public_function(5), 11); } } ``` ### When NOT to Use **Don't use this when:** - Integration tests (testing the public API only — use `tests/` dir) - Tests need separate binaries or processes - You're testing across multiple modules together --- ## 2. assert_eq! / assert_ne! with Descriptive Messages ### Source: 16,728 assert macros across the stdlib test code. `assert_eq!` is overwhelmingly preferred over raw `assert!` for comparisons. ```rust assert_eq!(result, expected); assert_eq!(result, expected, "failed for input {input}"); assert_ne!(left, right, "values should differ after mutation"); ``` ### Why `assert_eq!` prints both values on failure. `assert!(a == b)` only says "assertion failed" — useless for debugging. Always use the specific macro that matches your assertion type. ### When to Use **Triggers:** - Comparing two values: use `assert_eq!` - Values should differ: use `assert_ne!` - Boolean condition: use `assert!` - All: add context message for non-obvious failures **Example — before:** ```rust #[test] fn test_parse() { let result = parse("42"); assert!(result == Ok(42)); // "assertion failed" — which part failed? } ``` **Example — after:** ```rust #[test] fn test_parse() { assert_eq!(parse("42"), Ok(42)); // On failure: "left: Err(ParseError), right: Ok(42)" assert_eq!( parse("not_a_number"), Err(ParseError::InvalidDigit), "parsing non-numeric string should give InvalidDigit" ); } ``` ### When NOT to Use **Don't use this when:** - Comparing floats (use approximate comparison: `(a - b).abs() < epsilon`) - The Debug output is huge and unhelpful (write a custom assertion) --- ## 3. #[should_panic] for Panic Testing ### Source: 275+ `#[should_panic]` tests across the stdlib. ```rust #[test] #[should_panic(expected = "index out of bounds")] fn test_index_out_of_bounds() { let v = vec![1, 2, 3]; let _ = v[99]; } ``` ### Why Tests that SHOULD panic need explicit marking. Without `#[should_panic]`, the test framework considers a panic a failure. The `expected` parameter verifies the panic message contains a specific substring — prevents the test from passing due to an unrelated panic. ### When to Use **Triggers:** - Testing that invalid input causes a panic - Verifying invariant violations panic correctly - Testing `unwrap()` / `expect()` behavior **Example — before:** ```rust #[test] fn test_divide_by_zero_panics() { // This test always FAILS because the panic propagates divide(1, 0); // panics — test reports failure } ``` **Example — after:** ```rust #[test] #[should_panic(expected = "division by zero")] fn test_divide_by_zero_panics() { divide(1, 0); // panics with "division by zero" — test PASSES } ``` ### When NOT to Use **Don't use this when:** - You want to verify the panic payload is a specific type (use `std::panic::catch_unwind`) - The function should return an error instead of panicking (fix the function!) - You need to test what happens AFTER the panic (the test ends at the panic point) --- ## 4. Integration Tests (tests/ Directory) ### Source: The stdlib uses both `#[cfg(test)]` modules and separate test crates (`coretests/`, `alloctests/`). ```text my_crate/ ├── src/ │ └── lib.rs └── tests/ ├── integration_test.rs ← each file is a separate crate └── common/ └── mod.rs ← shared test utilities ``` ### Why Integration tests only access the public API. They compile as separate crates. This tests what users actually experience. Each file in `tests/` is a separate binary — they run in parallel. ### When to Use **Triggers:** - Testing the public API from a user's perspective - Testing interactions between multiple modules - Ensuring your library works as a dependency **Example — before:** ```rust // All tests in src/lib.rs — mixing unit and integration concerns #[cfg(test)] mod tests { #[test] fn test_internal_detail() { ... } // unit test #[test] fn test_user_workflow() { ... } // integration test — doesn't belong here } ``` **Example — after:** ```rust // tests/user_workflow.rs — clean integration test use my_crate::{Config, Server}; #[test] fn server_starts_and_handles_request() { let config = Config::default(); let server = Server::new(config); let response = server.handle("GET /"); assert_eq!(response.status(), 200); } ``` ### When NOT to Use **Don't use this when:** - You need to test private internals (use `#[cfg(test)]` module) - The test is quick and doesn't need isolation (unit test is fine) --- ## 5. #[track_caller] for Better Error Location ### Source: [library/core/src/panicking.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/panicking.rs) 389 `#[track_caller]` annotations in the stdlib. ```rust #[track_caller] pub fn unwrap(self) -> T { match self { Some(val) => val, None => panic!("called `Option::unwrap()` on a `None` value"), } } ``` ### Why Without `#[track_caller]`, a panic in a helper function shows the helper's location, not where it was called from. This attribute makes the panic point to the CALLER — where the bug actually is. ### When to Use **Triggers:** - Helper functions that panic (custom assert functions) - Wrapper functions around `unwrap()`/`expect()` - Any function where the caller is responsible for preconditions **Example — before:** ```rust fn assert_valid_port(port: u16) { assert!(port > 0 && port < 65535, "invalid port: {port}"); // Panic location: this line ← not helpful } #[test] fn test_connection() { assert_valid_port(0); // user wants to see THIS line in the error } ``` **Example — after:** ```rust #[track_caller] fn assert_valid_port(port: u16) { assert!(port > 0 && port < 65535, "invalid port: {port}"); // Panic location: the CALLER's line ← helpful! } #[test] fn test_connection() { assert_valid_port(0); // error points here — where the bug is } ``` ### When NOT to Use **Don't use this when:** - The function doesn't panic - The panic is an internal bug (the caller ISN'T at fault) - Performance-critical hot paths (small overhead from tracking) --- ## 6. Test Helpers and Shared Fixtures ### Source: The stdlib uses helper modules for shared test infrastructure. ```rust // tests/common/mod.rs — shared test utilities pub fn setup_temp_dir() -> PathBuf { ... } pub fn create_test_file(dir: &Path, name: &str) -> PathBuf { ... } ``` ### Why DRY test setup. Common fixtures, builders, and assertions live in shared modules. This reduces boilerplate without sacrificing clarity. ### When to Use **Triggers:** - Multiple tests need the same setup/teardown - Custom assertion logic used across test files - Test data builders (construct complex test objects) **Example — before:** ```rust #[test] fn test_parse_valid() { let input = r#"{"name": "test", "port": 8080}"#; let config = Config { name: "test".into(), port: 8080, ..Default::default() }; assert_eq!(parse(input).unwrap(), config); } #[test] fn test_parse_with_tls() { let input = r#"{"name": "test", "port": 443, "tls": true}"#; let config = Config { name: "test".into(), port: 443, tls: true, ..Default::default() }; assert_eq!(parse(input).unwrap(), config); } // Duplicated config construction everywhere ``` **Example — after:** ```rust fn config_builder() -> ConfigBuilder { ConfigBuilder::new().name("test").port(8080) } #[test] fn test_parse_valid() { let input = r#"{"name": "test", "port": 8080}"#; assert_eq!(parse(input).unwrap(), config_builder().build()); } #[test] fn test_parse_with_tls() { let input = r#"{"name": "test", "port": 443, "tls": true}"#; assert_eq!(parse(input).unwrap(), config_builder().port(443).tls(true).build()); } ``` --- ## 7. Doc Tests as Executable Examples ### Source: 144 doc test blocks in option.rs alone. Every public function has testable examples in its documentation. ```rust /// ``` /// let x: Option = Some(2); /// assert_eq!(x.unwrap(), 2); /// ``` ``` ### Why Doc tests serve dual purpose: they document behavior AND verify correctness. `cargo test` runs all doc tests. When the implementation changes, broken doc tests force documentation updates. ### When to Use **Triggers:** - Demonstrating how to use a public API - Showing expected input/output pairs - Proving that examples in docs are correct ### When NOT to Use **Don't use this when:** - The test requires complex setup (use `#[test]` function instead) - Testing edge cases not relevant to documentation - Performance testing (doc tests have startup overhead) --- ## 8. Testing Error Conditions ### Source: Extensive error case testing throughout the stdlib. ```rust #[test] fn parse_empty_string_returns_error() { let result: Result = "".parse(); assert!(result.is_err()); } #[test] fn file_not_found_error_kind() { let err = fs::read("/nonexistent").unwrap_err(); assert_eq!(err.kind(), io::ErrorKind::NotFound); } ``` ### Why Test the error paths, not just the happy path. Verify WHICH error occurs, not just that "some error" happens. Pattern match on error variants to ensure correct error classification. ### When to Use **Triggers:** - Every function that returns Result - Boundary conditions (empty input, zero, max value) - Invalid state transitions **Example — before:** ```rust #[test] fn test_parse_errors() { assert!(parse("").is_err()); // passes, but which error? assert!(parse("abc").is_err()); // could be any error } ``` **Example — after:** ```rust #[test] fn parse_empty_gives_empty_input_error() { assert_eq!(parse(""), Err(ParseError::EmptyInput)); } #[test] fn parse_non_numeric_gives_invalid_digit() { assert_eq!(parse("abc"), Err(ParseError::InvalidDigit('a'))); } #[test] fn parse_overflow_gives_overflow_error() { assert_eq!( parse("999999999999999999"), Err(ParseError::Overflow { max: u32::MAX }) ); } ``` --- ## 9. Conditional Compilation for Platform Tests ### Source: ```rust #[test] #[cfg(unix)] fn test_unix_permissions() { use std::os::unix::fs::PermissionsExt; let perms = fs::metadata("/tmp").unwrap().permissions(); assert!(perms.mode() & 0o777 != 0); } #[test] #[cfg(windows)] fn test_windows_path() { assert!(Path::new("C:\\").is_absolute()); } ``` ### Why Platform-specific tests only compile on the target platform. This prevents compilation errors on other platforms while still testing platform-specific behavior where it matters. ### When to Use **Triggers:** - Testing OS-specific APIs (Unix permissions, Windows paths) - Testing platform-specific behavior - Features only available on certain architectures ### When NOT to Use **Don't use this when:** - The test can be written in a platform-independent way - You're using cfg to SKIP broken tests (fix them instead) --- ## 10. Property-Based Testing (Invariant Verification) ### Source: 50 property-test-like patterns in the stdlib. The standard library uses exhaustive and randomized testing for numeric operations: ```rust // From coretests — testing mathematical properties #[test] fn test_add_is_commutative() { for a in 0..100u32 { for b in 0..100u32 { assert_eq!(a.wrapping_add(b), b.wrapping_add(a)); } } } ``` ### Why Property-based tests verify invariants hold for ALL inputs, not just hand-picked examples. They catch edge cases that example-based tests miss. Even without a full property testing framework, you can test properties with loops and random inputs. ### When to Use **Triggers:** - Mathematical properties (commutativity, associativity, idempotency) - Round-trip invariants (encode then decode = original) - Ordering properties (sort then check sorted) - Any "for all X, property Y holds" **Example — before:** ```rust #[test] fn test_serialize_roundtrip() { let value = MyStruct { x: 42, y: "hello".into() }; let bytes = serialize(&value); let result = deserialize(&bytes).unwrap(); assert_eq!(result, value); // Tests ONE specific input — what about empty strings? Large numbers? } ``` **Example — after:** ```rust #[test] fn serialize_roundtrip_property() { // Test with many inputs — catches edge cases let test_cases = vec![ MyStruct { x: 0, y: "".into() }, MyStruct { x: u32::MAX, y: "hello".into() }, MyStruct { x: 1, y: "a".repeat(10000) }, MyStruct { x: 42, y: "\0\n\t".into() }, ]; for value in test_cases { let bytes = serialize(&value); let result = deserialize(&bytes) .unwrap_or_else(|e| panic!("roundtrip failed for {value:?}: {e}")); assert_eq!(result, value, "roundtrip failed for {value:?}"); } } // With proptest crate: proptest! { #[test] fn roundtrip(x in any::(), y in ".*") { let value = MyStruct { x, y }; let bytes = serialize(&value); let result = deserialize(&bytes).unwrap(); prop_assert_eq!(result, value); } } ``` --- ## Summary: Testing Decision Tree ``` What are you testing? ├── Private internals → #[cfg(test)] mod tests in same file ├── Public API from user perspective → tests/ directory ├── Documentation correctness → Doc tests (```) └── Platform-specific behavior → #[cfg(target_os)] on test What kind of assertion? ├── Two values equal → assert_eq!(left, right) ├── Two values differ → assert_ne!(left, right) ├── Boolean condition → assert!(condition) ├── Should panic → #[should_panic(expected = "...")] ├── Should error → assert_eq!(result, Err(specific_error)) └── Property holds for all inputs → Loop/proptest Need shared setup? ├── Simple → helper function in test module ├── Complex → tests/common/mod.rs └── Custom assertion → #[track_caller] helper function ``` | Pattern | Use when | |---|---| | `#[cfg(test)] mod tests` | Unit testing (private access) | | `tests/` directory | Integration testing (public API) | | `assert_eq!` / `assert_ne!` | Comparing values (shows both on failure) | | `#[should_panic]` | Verifying panics occur | | `#[track_caller]` | Custom assert helpers (better error location) | | Doc tests | Executable documentation examples | | Error testing | Every `Result`-returning function | | `#[cfg(unix/windows)]` | Platform-specific tests | | Property-based | Invariant verification (roundtrips, math) | | Shared helpers | DRY test setup/fixtures | See also: - [documentation.md](documentation.md) — Doc test attributes - [error-handling.md](error-handling.md) — Error types worth testing - [api-design.md](api-design.md) — Testing public API contracts