Rust Testing Patterns TDD is a development claude skill built by Affaan M. Best for: Rust developers writing reliable, maintainable test suites following TDD methodology for production applications..

What it does
Implement comprehensive Rust testing patterns including unit, integration, async, property-based tests, and mocking.
Category
development
Created by
Affaan M
Last updated
Claude Skilldevelopment GitHub-backed CuratedintermediateClaude Code

Rust Testing Patterns TDD

Implement comprehensive Rust testing patterns including unit, integration, async, property-based tests, and mocking.

Skill instructions


name: rust-testing description: Rust测试模式,包括单元测试、集成测试、异步测试、基于属性的测试、模拟和覆盖率。遵循TDD方法学。 origin: ECC

Rust 测试模式

遵循 TDD 方法论编写可靠、可维护测试的全面 Rust 测试模式。

何时使用

  • 编写新的 Rust 函数、方法或特征
  • 为现有代码添加测试覆盖率
  • 为性能关键代码创建基准测试
  • 为输入验证实现基于属性的测试
  • 在 Rust 项目中遵循 TDD 工作流

工作原理

  1. 识别目标代码 — 找到要测试的函数、特征或模块
  2. 编写测试 — 在 #[cfg(test)] 模块中使用 #[test],使用 rstest 进行参数化测试,或使用 proptest 进行基于属性的测试
  3. 模拟依赖项 — 使用 mockall 来隔离被测单元
  4. 运行测试 (RED) — 验证测试是否按预期失败
  5. 实现 (GREEN) — 编写最少代码以通过测试
  6. 重构 — 改进代码同时保持测试通过
  7. 检查覆盖率 — 使用 cargo-llvm-cov,目标 80% 以上

Rust 的 TDD 工作流

RED-GREEN-REFACTOR 循环

RED     → 先写一个失败的测试
GREEN   → 编写最少代码使测试通过
REFACTOR → 重构代码,同时保持测试通过
REPEAT  → 继续下一个需求

Rust 中的分步 TDD

// 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'
// 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

单元测试

模块级测试组织

// 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"));
    }
}

断言宏

assert_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

错误与 Panic 测试

测试 Result 返回值

#[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
}

测试 Panic

#[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];
}

集成测试

文件结构

my_crate/
├── src/
│   └── lib.rs
├── tests/              # 集成测试
│   ├── api_test.rs     # 每个文件都是一个独立的测试二进制文件
│   ├── db_test.rs
│   └── common/         # 共享测试工具
│       └── mod.rs

编写集成测试

// 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");
}

异步测试

使用 Tokio

#[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 进行参数化测试

use 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()));
}

测试辅助函数

#[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 进行基于属性的测试

基本属性测试

use 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]);
        }
    }
}

自定义策略

use 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 进行模拟

基于特征的模拟

use 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());
}

文档测试

可执行的文档

/// 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!()
}

使用 Criterion 进行基准测试

# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

[[bench]]
name = "benchmark"
harness = false
// 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);

测试覆盖率

运行覆盖率

# 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 绑定 | 排除 |

测试命令

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        # 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

最佳实践

应该做:

  • 先写测试 (TDD)
  • 使用 #[cfg(test)] 模块进行单元测试
  • 测试行为,而非实现
  • 使用描述性测试名称来解释场景
  • 为了更好的错误信息,优先使用 assert_eq! 而非 assert!
  • 在返回 Result 的测试中使用 ? 以获得更清晰的错误输出
  • 保持测试独立 — 没有共享的可变状态

不应该做:

  • 在可以测试 Result::is_err() 时使用 #[should_panic]
  • 模拟所有内容 — 在可行时优先考虑集成测试
  • 忽略不稳定的测试 — 修复或隔离它们
  • 在测试中使用 sleep() — 使用通道、屏障或 tokio::time::pause()
  • 跳过错误路径测试

CI 集成

# 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

记住:测试就是文档。它们展示了你的代码应如何使用。清晰编写并保持更新。

Use this skill

Most skills are portable instruction packages. Claude Code supports SKILL.md directly. Other agents can use adapted files like AGENTS.md, .cursorrules, and GEMINI.md.

Claude Code

Save SKILL.md into your Claude Skills folder, then restart Claude Code.

mkdir -p ~/.claude/skills/rust-testing-patterns-tdd && curl -L "https://raw.githubusercontent.com/affaan-m/everything-claude-code/HEAD/docs/zh-CN/skills/rust-testing/SKILL.md" -o ~/.claude/skills/rust-testing-patterns-tdd/SKILL.md

Installs to ~/.claude/skills/rust-testing-patterns-tdd/SKILL.md.

Use cases

Rust developers writing reliable, maintainable test suites following TDD methodology for production applications.

Reviews

No reviews yet. Be the first to review this skill.

No signup required

Stats

Installs0
GitHub Stars174.9k
Forks27058
LicenseMIT
UpdatedMar 27, 2026