Communitygithub.com

Rust 测试模式

涵盖单元测试、集成测试、异步测试、属性测试、Mock 和覆盖率分析的 Rust 测试模式。遵循 TDD 方法论。在编写 Rust 测试(单元、集成、异步、属性或覆盖率)时使用。

Rust 测试模式 是什么?

该技能为 Rust 开发者提供了一套全面的测试模式与实践,严格遵循 TDD 的“红-绿-重构”循环。它详细介绍了多种测试类型:使用 `#[test]` 和 `#[cfg(test)]` 模块的单元测试、放在 `tests/` 目录下的集成测试、借助 `#[tokio::test]` 处理的异步测试,以及利用 `proptest` 进行的属性测试。此外,该技能还深入介绍了如何使用 `mockall` 进行基于 trait 的 Mock,以隔离被测单元;使用 `rstest` 编写参数化测试和 Fixture;并通过 `cargo-llvm-cov` 提供代码覆盖率目标(关键逻辑 100%,通用代码 80%+)。它还包含了测试 `Result` 返回和 `panic` 场景的最佳实践,并提供了 `criterion` 基准测试和 GitHub Actions CI 集成的完整示例,旨在帮助开发者编写可靠、可维护且具备自文档能力的测试。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/rust-testing

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Rust 测试模式 是做什么的?

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

何时使用

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

工作原理

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

Rust 的 TDD 工作流

RED-GREEN-REFACTOR 循环

RED     → 首先编写一个失败的测试
GREEN   → 编写最简代码以通过测试
REFACTOR → 在保持测试通过的前提下改进代码
REPEAT  → 继续下一个需求

Rust 中的逐步 TDD

// RED: 先编写测试,使用 todo!() 作为占位符
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: 将 todo!() 替换为最简实现
pub fn add(a: i32, b: i32) -> i32 { a + b }
// cargo test → PASS,然后 GREGON 在保持测试通过的前提下重构

单元测试

模块级测试组织

// 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", "[email protected]").unwrap();
        assert_eq!(user.display_name(), "Alice");
        assert_eq!(user.email, "[email protected]");
    }

    #[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);                                    // 相等
assert_ne!(2 + 2, 5);                                    // 不等
assert!(vec![1, 2, 3].contains(&2));                     // 布尔值
assert_eq!(value, 42, "expected 42 but got {value}");    // 自定义消息
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON);   // 浮点数比较

错误和 Panic 测试

测试 Result 返回值

#[test]
fn parse_returns_error_for_invalid_input() {
    let result = parse_config("}{"invalid);
    assert!(result.is_err());

    // 断言特定的错误变体
    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(()) // 如果任何 ? 返回 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::*;

    /// 使用合理的默认值创建一个测试用户。
    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 进行 Mock

基于 Trait 的 Mock

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

文档测试

可执行的文档

/// 将两个数相加。
///
/// # 示例
///
/// ```
/// 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
}

/// 解析配置字符串。
///
/// # 错误
///
/// 如果输入不是有效的 TOML,返回 `Err`。
///
/// ```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);

测试覆盖率

运行覆盖率

# 安装:cargo install cargo-llvm-cov(或在 CI 中使用 taiki-e/install-action)
cargo llvm-cov                    # 摘要
cargo llvm-cov --html             # HTML 报告
cargo llvm-cov --lcov > lcov.info # LCOV 格式用于 CI
cargo llvm-cov --fail-under-lines 80  # 如果低于阈值则失败

覆盖率目标

代码类型目标
关键业务逻辑100%
公共 API90%+
通用代码80%+
生成的 / FFI 绑定排除

测试命令

cargo test                        # 运行所有测试
cargo test -- --nocapture         # 显示 println 输出
cargo test test_name              # 运行匹配模式的测试
cargo test --lib                  # 仅单元测试
cargo test --test api_test        # 仅集成测试
cargo test --doc                  # 仅文档测试
cargo test --no-fail-fast         # 不在第一次失败时停止
cargo test -- --ignored           # 运行被忽略的测试

最佳实践

应该做:

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

不应该做:

  • 当可以测试 Result::is_err() 时,不要使用 #[should_panic]
  • 不要 Mock 所有内容——在可行时优先使用集成测试
  • 不要忽略不稳定的测试——要么修复,要么隔离
  • 不要在测试中使用 sleep()——使用 channel、barrier 或 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: 检查格式
      run: cargo fmt --check

    - name: Clippy
      run: cargo clippy -- -D warnings

    - name: 运行测试
      run: cargo test

    - uses: taiki-e/install-action@cargo-llvm-cov

    - name: 覆盖率
      run: cargo llvm-cov --fail-under-lines 80

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

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能