Communitygithub.com

Rust 測試模式

涵蓋單元測試、整合測試、非同步測試、屬性基礎測試、模擬(mocking)與程式碼涵蓋率的 Rust 測試模式。遵循 TDD 方法論。在編寫 Rust 測試時使用——無論是單元測試、整合測試、非同步測試、屬性基礎測試還是涵蓋率測試。

Rust 測試模式 是什麼?

這項技能為使用 Rust 進行測試提供了一套全面的模式與最佳實踐,遵循測試驅動開發(TDD)方法論。它涵蓋了 Rust 測試的各個面向,包括單元測試、整合測試、非同步測試、屬性基礎測試(使用 proptest)、模擬(使用 mockall)以及程式碼涵蓋率(使用 cargo-llvm-cov)。技能詳細說明了標準的 TDD 紅綠重構循環,並提供清晰範例,展示如何組織測試模組、撰寫有效的斷言巨集、處理錯誤與恐慌情況,以及使用 rstest 進行參數化測試。它還包含了使用 Criterion 進行基準測試的指導,整合到 CI/CD 管線的步驟(例如 GitHub Actions),以及保持測試可靠、獨立且富有文檔價值的最佳實踐。這項技能支援所有主要的 Rust 編輯器與平台,包括 VS Code、IntelliJ、Neovim、Helix、Zed 及 GitHub Codespaces。

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

在你喜歡的 AI 中提問

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

說明文件

Rust 測試模式 是做什麼的?

全面的 Rust 測試模式,幫助你依照 TDD 方法論撰寫可靠、可維護的測試。

使用時機

  • 撰寫新的 Rust 函式、方法或 trait 時
  • 為現有程式碼增加測試涵蓋率時
  • 為效能關鍵程式碼建立基準測試時
  • 為輸入驗證實作屬性基礎測試時
  • 在 Rust 專案中遵循 TDD 工作流程時

運作方式

  1. 識別目標程式碼 — 找出要測試的函式、trait 或模組
  2. 撰寫測試 — 在 #[cfg(test)] 模組中使用 #[test],或使用 rstest 進行參數化測試,或使用 proptest 進行屬性基礎測試
  3. 模擬相依項目 — 使用 mockall 來隔離被測試的單元
  4. 執行測試(紅燈) — 確認測試因預期錯誤而失敗
  5. 實作(綠燈) — 撰寫最簡潔的程式碼以通過測試
  6. 重構 — 在維持測試綠燈的同時進行改善
  7. 檢查涵蓋率 — 使用 cargo-llvm-cov,目標為 80% 以上

Rust 的 TDD 工作流程

紅燈-綠燈-重構循環

紅燈     → 先撰寫一個會失敗的測試
綠燈     → 撰寫最簡潔的程式碼以通過測試
重構      → 在維持測試綠燈的同時改善程式碼
重複      → 繼續處理下一個需求

Rust 中的逐步 TDD

// 紅燈:先撰寫測試,使用 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 → 因「未實作」而恐慌
// 綠燈:將 todo!() 替換為最簡潔的實作
pub fn add(a: i32, b: i32) -> i32 { a + b }
// cargo test → 通過,接著在維持測試綠燈的同時進行重構

單元測試

模組層級的測試組織

// 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!("無效的電子郵件:{email}"));
        }
        Ok(Self { name: name.into(), email })
    }

    pub fn display_name(&self) -> &str {
        &self.name
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn 使用有效的電子郵件建立使用者() {
        let user = User::new("Alice", "[email protected]").unwrap();
        assert_eq!(user.display_name(), "Alice");
        assert_eq!(user.email, "[email protected]");
    }

    #[test]
    fn 拒絕無效的電子郵件() {
        let result = User::new("Bob", "not-an-email");
        assert!(result.is_err());
        assert!(result.unwrap_err().contains("無效的電子郵件"));
    }
}

斷言巨集

assert_eq!(2 + 2, 4);                                    // 相等
assert_ne!(2 + 2, 5);                                    // 不相等
assert!(vec![1, 2, 3].contains(&2));                     // 布林值
assert_eq!(value, 42, "預期 42 但得到 {value}");        // 自訂訊息
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON);   // 浮點數比較

錯誤與恐慌測試

測試 Result 回傳值

#[test]
fn 對無效輸入回傳錯誤() {
    let result = parse_config("}{"invalid);
    assert!(result.is_err());

    // 斷言特定的錯誤變體
    let err = result.unwrap_err();
    assert!(matches!(err, ConfigError::ParseError(_)));
}

#[test]
fn 對有效輸入成功解析() -> Result<(), Box<dyn std::error::Error>> {
    let config = parse_config(r#"{"port": 8080}"#)?;
    assert_eq!(config.port, 8080);
    Ok(()) // 如果任何 ? 回傳 Err,測試就會失敗
}

測試恐慌

#[test]
#[should_panic]
fn 對空輸入引發恐慌() {
    process(&[]);
}

#[test]
#[should_panic(expected = "索引超出範圍")]
fn 伴隨特定訊息引發恐慌() {
    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 完整的請求生命週期() {
    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 成功擷取資料() {
    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 處理逾時() {
    use std::time::Duration;
    let result = tokio::time::timeout(
        Duration::from_millis(100),
        slow_operation(),
    ).await;

    assert!(result.is_err(), "應已逾時");
}

測試組織模式

使用 rstest 進行參數化測試

use rstest::{rstest, fixture};

#[rstest]
#[case("hello", 5)]
#[case("", 0)]
#[case("rust", 4)]
fn 測試字串長度(#[case] input: &str, #[case] expected: usize) {
    assert_eq!(input.len(), expected);
}

// 裝備(Fixtures)
#[fixture]
fn test_db() -> TestDb {
    TestDb::new_in_memory()
}

#[rstest]
fn 測試插入(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 使用者顯示名稱() {
        let user = make_user("alice");
        assert_eq!(user.display_name(), "alice");
    }
}

使用 proptest 進行屬性基礎測試

基本屬性測試

use proptest::prelude::*;

proptest! {
    #[test]
    fn 編碼解碼往返測試(input in ".*") {
        let encoded = encode(&input);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(input, decoded);
    }

    #[test]
    fn 排序保留長度(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 排序產生有序輸出(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 接受有效的電子郵件(email in valid_email()) {
        assert!(User::new("Test", &email).is_ok());
    }
}

使用 mockall 進行模擬

基於 Trait 的模擬

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 服務在使用者存在時回傳使用者() {
    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 服務在使用者不存在時回傳 None() {
    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()——應使用通道、屏障或 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/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

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.

相關技能