Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Rust testing patterns including unit tests, integration tests, async testing, property-based testing, mocking, and coverage. Follows TDD methodology.
.claude/skills/loulanyue-rust-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | 421% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 203% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 174% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 319% | 0% |
Comprehensive Rust testing patterns for writing reliable, maintainable tests following TDD methodology.
#[test] in a #[cfg(test)] module, rstest for parameterized tests, or proptest for property-based testsRED → Write a failing test first
GREEN → Write minimal code to pass the test
REFACTOR → Improve code while keeping tests green
REPEAT → Continue with next requirementrust// RED: Write test first, use todo!() as placeholder pub fn add(a: i32, b: i32) -> i32 { todo!() } #[cfg(test)] mod tests { use super::*; #[test] fn test_add() { assert_eq!(add(2, 3), 5); } } // cargo test → panics at 'not yet implemented'
rust// GREEN: Replace todo!() with minimal implementation pub fn add(a: i32, b: i32) -> i32 { a + b } // cargo test → PASS, then REFACTOR while keeping tests green
rust// src/user.rs pub struct User { pub name: String, pub email: String, } impl User { pub fn new(name: impl Into<String>, email: impl Into<String>) -> Result<Self, String> { let email = email.into(); if !email.contains('@') { return Err(format!("invalid email: {email}")); } Ok(Self { name: name.into(), email }) } pub fn display_name(&self) -> &str { &self.name } } #[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.display_name(), "Alice"); assert_eq!(user.email, "alice@example.com"); } #[test] fn rejects_invalid_email() { let result = User::new("Bob", "not-an-email"); assert!(result.is_err()); assert!(result.unwrap_err().contains("invalid email")); } }
rustassert_eq!(2 + 2, 4); // Equality assert_ne!(2 + 2, 5); // Inequality assert!(vec![1, 2, 3].contains(&2)); // Boolean assert_eq!(value, 42, "expected 42 but got {value}"); // Custom message assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON); // Float comparison
Result Returnsrust#[test] fn parse_returns_error_for_invalid_input() { let result = parse_config("}{invalid"); assert!(result.is_err()); // Assert specific error variant let err = result.unwrap_err(); assert!(matches!(err, ConfigError::ParseError(_))); } #[test] fn parse_succeeds_for_valid_input() -> Result<(), Box<dyn std::error::Error>> { let config = parse_config(r#"{"port": 8080}"#)?; assert_eq!(config.port, 8080); Ok(()) // Test fails if any ? returns Err }
rust#[test] #[should_panic] fn panics_on_empty_input() { process(&[]); } #[test] #[should_panic(expected = "index out of bounds")] fn panics_with_specific_message() { let v: Vec<i32> = vec![]; let _ = v[0]; }
textmy_crate/ ├── src/ │ └── lib.rs ├── tests/ # Integration tests │ ├── api_test.rs # Each file is a separate test binary │ ├── db_test.rs │ └── common/ # Shared test utilities │ └── mod.rs
rust// tests/api_test.rs use my_crate::{App, Config}; #[test] fn full_request_lifecycle() { let config = Config::test_default(); let app = App::new(config); let response = app.handle_request("/health"); assert_eq!(response.status, 200); assert_eq!(response.body, "OK"); }
rust#[tokio::test] async fn fetches_data_successfully() { let client = TestClient::new().await; let result = client.get("/data").await; assert!(result.is_ok()); assert_eq!(result.unwrap().items.len(), 3); } #[tokio::test] async fn handles_timeout() { use std::time::Duration; let result = tokio::time::timeout( Duration::from_millis(100), slow_operation(), ).await; assert!(result.is_err(), "should have timed out"); }
rstestrustuse rstest::{rstest, fixture}; #[rstest] #[case("hello", 5)] #[case("", 0)] #[case("rust", 4)] fn test_string_length(#[case] input: &str, #[case] expected: usize) { assert_eq!(input.len(), expected); } // Fixtures #[fixture] fn test_db() -> TestDb { TestDb::new_in_memory() } #[rstest] fn test_insert(test_db: TestDb) { test_db.insert("key", "value"); assert_eq!(test_db.get("key"), Some("value".into())); }
rust#[cfg(test)] mod tests { use super::*; /// Creates a test user with sensible defaults. fn make_user(name: &str) -> User { User::new(name, &format!("{name}@test.com")).unwrap() } #[test] fn user_display() { let user = make_user("alice"); assert_eq!(user.display_name(), "alice"); } }
proptestrustuse proptest::prelude::*; proptest! { #[test] fn encode_decode_roundtrip(input in ".*") { let encoded = encode(&input); let decoded = decode(&encoded).unwrap(); assert_eq!(input, decoded); } #[test] fn sort_preserves_length(mut vec in prop::collection::vec(any::<i32>(), 0..100)) { let original_len = vec.len(); vec.sort(); assert_eq!(vec.len(), original_len); } #[test] fn sort_produces_ordered_output(mut vec in prop::collection::vec(any::<i32>(), 0..100)) { vec.sort(); for window in vec.windows(2) { assert!(window[0] <= window[1]); } } }
rustuse proptest::prelude::*; fn valid_email() -> impl Strategy<Value = String> { ("[a-z]{1,10}", "[a-z]{1,5}") .prop_map(|(user, domain)| format!("{user}@{domain}.com")) } proptest! { #[test] fn accepts_valid_emails(email in valid_email()) { assert!(User::new("Test", &email).is_ok()); } }
mockallrustuse mockall::{automock, predicate::eq}; #[automock] trait UserRepository { fn find_by_id(&self, id: u64) -> Option<User>; fn save(&self, user: &User) -> Result<(), StorageError>; } #[test] fn service_returns_user_when_found() { let mut mock = MockUserRepository::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] fn service_returns_none_when_not_found() { let mut mock = MockUserRepository::new(); mock.expect_find_by_id() .returning(|_| None); let service = UserService::new(Box::new(mock)); assert!(service.get_user(99).is_none()); }
rust/// Adds two numbers together. /// /// # Examples /// /// ``` /// use my_crate::add; /// /// assert_eq!(add(2, 3), 5); /// assert_eq!(add(-1, 1), 0); /// ``` pub fn add(a: i32, b: i32) -> i32 { a + b } /// Parses a config string. /// /// # Errors /// /// Returns `Err` if the input is not valid TOML. /// /// ```no_run /// use my_crate::parse_config; /// /// let config = parse_config(r#"port = 8080"#).unwrap(); /// assert_eq!(config.port, 8080); /// ``` /// /// ```no_run /// use my_crate::parse_config; /// /// assert!(parse_config("}{invalid").is_err()); /// ``` pub fn parse_config(input: &str) -> Result<Config, ParseError> { todo!() }
toml# Cargo.toml [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } [[bench]] name = "benchmark" harness = false
rust// benches/benchmark.rs use criterion::{black_box, criterion_group, criterion_main, Criterion}; fn fibonacci(n: u64) -> u64 { match n { 0 | 1 => n, _ => fibonacci(n - 1) + fibonacci(n - 2), } } fn bench_fibonacci(c: &mut Criterion) { c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20)))); } criterion_group!(benches, bench_fibonacci); criterion_main!(benches);
bash# Install: cargo install cargo-llvm-cov (or use taiki-e/install-action in CI) cargo llvm-cov # Summary cargo llvm-cov --html # HTML report cargo llvm-cov --lcov > lcov.info # LCOV format for CI cargo llvm-cov --fail-under-lines 80 # Fail if below threshold
| Code Type | Target | |-----------|--------| | Critical business logic | 100% | | Public API | 90%+ | | General code | 80%+ | | Generated / FFI bindings | Exclude |
bashcargo 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 # Integration tests only cargo test --doc # Doc tests only cargo test --no-fail-fast # Don't stop on first failure cargo test -- --ignored # Run ignored tests
DO:
#[cfg(test)] modules for unit testsassert_eq! over assert! for better error messages? in tests that return Result for cleaner error outputDON'T:
#[should_panic] when you can test Result::is_err() insteadsleep() in tests — use channels, barriers, or tokio::time::pause()yaml# GitHub Actions test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - name: Check formatting run: cargo fmt --check - name: Clippy run: cargo clippy -- -D warnings - name: Run tests run: cargo test - uses: taiki-e/install-action@cargo-llvm-cov - name: Coverage run: cargo llvm-cov --fail-under-lines 80
Remember: Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 7,582 | 4,450 | -41% | 1 | 1 | 0% | 1,461 | 4,432 | +203% | 0 | 0 | — |
case-02 | pass→pass | 7,716 | 3,671 | -52% | 1 | 1 | 0% | 1,555 | 4,265 | +174% | 0 | 0 | — |
case-03 | pass→pass | 6,122 | 4,127 | -33% | 1 | 1 | 0% | 1,031 | 4,324 | +319% | 0 | 0 | — |
case-04 | pass→pass | 3,843 | 2,830 | -26% | 1 | 1 | 0% | 666 | 4,119 | +518% | 0 | 0 | — |
case-05 | pass→pass | 5,816 | 5,447 | -6% | 1 | 1 | 0% | 1,098 | 4,615 | +320% | 0 | 0 | — |
case-06 | pass→pass | 5,614 | 5,904 | +5% | 1 | 1 | 0% | 1,002 | 4,595 | +359% | 0 | 0 | — |
case-07 | pass→pass | 11,854 | 5,957 | -50% | 1 | 1 | 0% | 2,071 | 4,607 | +122% | 0 | 0 | — |
case-08 | pass→pass | 3,282 | 2,922 | -11% | 1 | 1 | 0% | 503 | 4,025 | +700% | 0 | 0 | — |
case-09 | pass→pass | 10,934 | 6,691 | -39% | 1 | 1 | 0% | 1,800 | 4,844 | +169% | 0 | 0 | — |
case-10 | pass→pass | 4,050 | 3,758 | -7% | 1 | 1 | 0% | 663 | 4,312 | +550% | 0 | 0 | — |
case-11 | fail→pass | 4,827 | 3,745 | -22% | 1 | 1 | 0% | 819 | 4,265 | +421% | 0 | 0 | — |
case-12 | fail→pass | 14,213 | 3,783 | -73% | 1 | 1 | 0% | 2,303 | 4,225 | +83% | 0 | 0 | — |
case-13 | pass→pass | 3,387 | 2,626 | -22% | 1 | 1 | 0% | 484 | 4,005 | +727% | 0 | 0 | — |
case-14 | pass→pass | 2,620 | 1,924 | -27% | 1 | 1 | 0% | 400 | 3,848 | +862% | 0 | 0 | — |
case-15 | pass→pass | 4,776 | 5,288 | +11% | 1 | 1 | 0% | 725 | 4,479 | +518% | 0 | 0 | — |
case-16 | pass→pass | 10,619 | 9,555 | -10% | 1 | 1 | 0% | 2,003 | 5,433 | +171% | 0 | 0 | — |
case-17 | pass→pass | 5,597 | 4,556 | -19% | 1 | 1 | 0% | 1,063 | 4,473 | +321% | 0 | 0 | — |
case-18 | pass→pass | 2,834 | 2,269 | -20% | 1 | 1 | 0% | 495 | 4,025 | +713% | 0 | 0 | — |
case-19 | pass→pass | 4,908 | 2,636 | -46% | 1 | 1 | 0% | 775 | 4,117 | +431% | 0 | 0 | — |
case-20 | pass→pass | 2,859 | 1,986 | -31% | 1 | 1 | 0% | 516 | 3,857 | +647% | 0 | 0 | — |
case-21 | pass→pass | 14,254 | 9,202 | -35% | 1 | 1 | 0% | 2,691 | 5,318 | +98% | 0 | 0 | — |
case-22 | pass→pass | 12,665 | 8,241 | -35% | 1 | 1 | 0% | 2,238 | 5,028 | +125% | 0 | 0 | — |
case-23 | pass→pass | 9,838 | 8,772 | -11% | 1 | 1 | 0% | 2,022 | 5,245 | +159% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.