Communitygithub.com

Rust 測試模式

涵蓋單元測試、整合測試、非同步測試、基於屬性的測試、模擬與覆蓋率的 Rust 測試模式。遵循 TDD 方法論。撰寫 Rust 測試時使用——單元、整合、非同步、基於屬性測試或覆蓋率。

Rust 測試模式 是什麼?

此技能提供了一套全面的 Rust 測試模式,旨在幫助開發者遵循測試驅動開發 (TDD) 方法來撰寫可靠且易於維護的測試。它支援多種測試類型,包括單元測試、整合測試、非同步測試、基於屬性的測試 (proptest) 以及文件測試,並涵蓋了使用 mockall 進行模擬、使用 cargo-llvm-cov 衡量覆蓋率以及使用 Criterion 進行效能基準測試。技能內容詳細說明了 TDD 的 RED-GREEN-REFACTOR 循環,並提供豐富的程式碼範例來說明各種測試模式的實作方式,例如參數化測試 (rstest)、錯誤與恐慌測試、以及自訂測試輔助函數。此外,它也包含了最佳實踐指南 (如先寫測試、測試行為而非實作) 和 CI 整合範例 (GitHub Actions),讓開發者能將完善的測試流程直接整合到專案中。

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

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Rust 測試模式 是做什麼的?

遵循 TDD 方法論,撰寫可靠、可維護測試的全面 Rust 測試模式。

使用時機

  • 撰寫新的 Rust 函式、方法或特徵 (trait)
  • 為現有程式碼增加測試覆蓋率
  • 為效能關鍵程式碼建立基準測試 (benchmark)
  • 為輸入驗證實作基於屬性的測試 (property-based test)
  • 在 Rust 專案中遵循 TDD 工作流程

運作方式

  1. 識別目標程式碼 — 找出要測試的函式、特徵或模組
  2. 撰寫測試 — 在 #[cfg(test)] 模組中使用 #[test],使用 rstest 進行參數化測試,或使用 proptest 進行基於屬性的測試
  3. 模擬依賴 — 使用 mockall 來隔離受測單元
  4. 執行測試 (RED) — 驗證測試因預期錯誤而失敗
  5. 實作 (GREEN) — 撰寫最小程式碼使其通過
  6. 重構 (Refactor) — 在維持測試通過的情況下改善程式碼
  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,然後在保持測試通過的情況下 REFACTOR

單元測試

模組層級測試組織

// 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);   // 浮點數比較

錯誤與恐慌測試

測試 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,測試就會失敗
}

測試恐慌

#[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 進行模擬

基於特徵的模擬

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 # 給 CI 用的 LCOV 格式
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]
  • 不要模擬所有東西——可行時優先使用整合測試
  • 不要忽略不穩定的測試——修復它們或將其隔離
  • 不要在測試中使用 sleep()——改用 channels、barriers 或 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.

相關技能