Communitygithub.com

Rust テストパターン

ユニットテスト、統合テスト、非同期テスト、プロパティベーステスト、モック、カバレッジを含むRustのテストパターンです。TDD手法に従います。Rustのテスト(ユニット、統合、非同期、プロパティベース、カバレッジ)を記述する際に使用します。

Rust テストパターン とは?

このスキルは、Rustプロジェクトにおけるテストパターンを包括的に提供し、TDD(テスト駆動開発)手法に従った信頼性が高く保守しやすいテストの作成を支援します。ユニットテスト、統合テスト、非同期テスト、プロパティベーステスト、モッキング、ベンチマーク、カバレッジ計測など、幅広いテスト手法をカバーしています。具体的には、`#[cfg(test)]` モジュールを使ったテスト構造、`rstest` によるパラメータ化テスト、`proptest` によるプロパティベーステスト、`mockall` によるトレイトベースのモック、`tokio::test` による非同期テスト、`criterion` によるベンチマーク、`cargo-llvm-cov` によるカバレッジレポートの生成方法を詳しく解説します。また、RED-GREEN-REFACTORサイクルに沿ったTDDのワークフローや、テストコマンド、CI統合のベストプラクティスも含まれています。このスキルは、初心者から上級者まで、あらゆるRust開発者が高品質なテストスイートを構築するための実践的なガイドとなります。

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

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

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: 最初にテストを書き、プレースホルダーとして 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 → '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);
}

// フィクスチャ
#[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());
}

ドキュメンテーションテスト

実行可能なドキュメント

/// 2つの数値を加算します。
///
/// # 例
///
/// ```
/// 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! よりも assert_eq! を優先する
  • よりクリーンなエラー出力のために、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.

関連スキル