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/affaan-m-rust-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 81% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 176% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 226% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 139% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 260% | 0% |
遵循 TDD 方法论编写可靠、可维护测试的全面 Rust 测试模式。
#[cfg(test)] 模块中使用 #[test],使用 rstest 进行参数化测试,或使用 proptest 进行基于属性的测试RED → 先写一个失败的测试
GREEN → 编写最少代码使测试通过
REFACTOR → 重构代码,同时保持测试通过
REPEAT → 继续下一个需求rust// 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 返回值rust#[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/ # 集成测试 │ ├── api_test.rs # 每个文件都是一个独立的测试二进制文件 │ ├── db_test.rs │ └── common/ # 共享测试工具 │ └── 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"); }
rstest 进行参数化测试rustuse 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"); } }
proptest 进行基于属性的测试rustuse 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()); } }
mockall 进行模拟rustuse 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
| 代码类型 | 目标 | |-----------|--------| | 关键业务逻辑 | 100% | | 公共 API | 90%+ | | 通用代码 | 80%+ | | 生成的 / FFI 绑定 | 排除 |
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
应该做:
#[cfg(test)] 模块进行单元测试assert_eq! 而非 assert!Result 的测试中使用 ? 以获得更清晰的错误输出不应该做:
Result::is_err() 时使用 #[should_panic]sleep() — 使用通道、屏障或 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
记住:测试就是文档。它们展示了你的代码应如何使用。清晰编写并保持更新。
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 11,134 | 9,105 | -18% | 1 | 1 | 0% | 2,421 | 5,457 | +125% | 0 | 0 | — |
case-02 | pass→pass | 15,429 | 5,262 | -66% | 1 | 1 | 0% | 2,409 | 4,388 | +82% | 0 | 0 | — |
case-03 | fail→pass | 13,883 | 1,842 | -87% | 1 | 1 | 0% | 2,130 | 3,858 | +81% | 0 | 0 | — |
case-04 | pass→pass | 9,470 | 4,163 | -56% | 1 | 1 | 0% | 1,969 | 4,449 | +126% | 0 | 0 | — |
case-05 | pass→pass | 9,816 | 6,977 | -29% | 1 | 1 | 0% | 1,773 | 4,830 | +172% | 0 | 0 | — |
case-06 | pass→pass | 8,241 | 5,213 | -37% | 1 | 1 | 0% | 1,247 | 4,600 | +269% | 0 | 0 | — |
case-07 | pass→pass | 15,802 | 18,279 | +16% | 1 | 1 | 0% | 2,545 | 6,600 | +159% | 0 | 0 | — |
case-08 | pass→pass | 5,793 | 5,389 | -7% | 1 | 1 | 0% | 1,201 | 4,625 | +285% | 0 | 0 | — |
case-09 | fail→pass | 9,040 | 6,960 | -23% | 1 | 1 | 0% | 1,774 | 4,898 | +176% | 0 | 0 | — |
case-10 | pass→pass | 9,212 | 6,358 | -31% | 1 | 1 | 0% | 1,814 | 4,880 | +169% | 0 | 0 | — |
case-11 | pass→pass | 19,098 | 7,782 | -59% | 1 | 1 | 0% | 1,786 | 5,029 | +182% | 0 | 0 | — |
case-12 | fail→pass | 9,207 | 9,917 | +8% | 1 | 1 | 0% | 1,672 | 5,450 | +226% | 0 | 0 | — |
case-13 | pass→pass | 8,732 | 8,912 | +2% | 1 | 1 | 0% | 1,655 | 4,937 | +198% | 0 | 0 | — |
case-14 | fail→fail | 10,472 | 8,363 | -20% | 1 | 1 | 0% | 1,926 | 5,252 | +173% | 0 | 0 | — |
case-15 | pass→pass | 3,781 | 4,363 | +15% | 1 | 1 | 0% | 691 | 4,405 | +537% | 0 | 0 | — |
case-16 | fail→pass | 9,237 | 2,646 | -71% | 1 | 1 | 0% | 1,695 | 4,049 | +139% | 0 | 0 | — |
case-17 | fail→pass | 6,170 | 3,409 | -45% | 1 | 1 | 0% | 1,161 | 4,175 | +260% | 0 | 0 | — |
case-18 | pass→pass | 4,023 | 2,434 | -39% | 1 | 1 | 0% | 751 | 4,033 | +437% | 0 | 0 | — |
case-19 | pass→pass | 2,899 | 2,411 | -17% | 1 | 1 | 0% | 488 | 3,967 | +713% | 0 | 0 | — |
case-20 | pass→pass | 9,592 | 8,903 | -7% | 1 | 1 | 0% | 2,009 | 5,379 | +168% | 0 | 0 | — |
case-21 | pass→pass | 10,071 | 8,341 | -17% | 1 | 1 | 0% | 1,671 | 5,142 | +208% | 0 | 0 | — |
case-22 | pass→pass | 11,196 | 12,500 | +12% | 1 | 1 | 0% | 2,119 | 6,021 | +184% | 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. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 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.