Communitygithub.com

Rustテストパターン

ユニットテスト、統合テスト、非同期テスト、プロパティベーステスト、モッキング、カバレッジを含むRustのテストパターン。TDD手法に従います。Rustのテスト(ユニット、統合、非同期、プロパティベース、カバレッジ)を書くときに使用します。

Rustテストパターン とは?

このスキルは、TDD(テスト駆動開発)手法に従った信頼性が高くメンテナブルなRustテストの包括的なパターンを提供します。Rustの関数、メソッド、トレイトのテスト作成、既存コードへのテストカバレッジ追加、パフォーマンス重視コードのベンチマーク作成、入力検証のためのプロパティベーステスト実装など、幅広いユースケースをカバーします。`#[test]`を使った基本的なユニットテストから、`rstest`を使ったパラメータ化テスト、`proptest`を使ったプロパティベーステスト、`mockall`を使ったモッキング、`tokio::test`を使った非同期テスト、`cargo-llvm-cov`を使ったカバレッジ計測まで、すべての主要なテスト手法を網羅しています。さらに、ドキュメントテスト(doc tests)やCriterionを使ったベンチマーク、CI統合設定についても詳しく説明しています。このスキルは、Rustのテストを体系的に学びたい開発者や、既存プロジェクトに堅牢なテスト基盤を導入したいチームにとって非常に役立ちます。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/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/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.

関連スキル