Rust Testing

affaan-m/ECC/docs/ja-JP/skills/rust-testing

by affaan-mef648e01899ba3e8dc6371642deaaf64b4477775No license275K starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated 4 days ago

単体テスト、統合テスト、非同期テスト、プロパティベーステスト、モック、カバレッジを含むRustテストパターン。TDD方法論に従う。

Instructions onlySoftware Development
AI-generated overview

Rust testing patterns covering unit, integration, async, property-based, mock, benchmark and coverage workflows with TDD.

What it does
This skill supplies reference patterns and code examples for writing tests in Rust: unit tests in #[cfg(test)] modules, integration tests under tests/, async tests with Tokio, parameterized tests with rstest, property-based tests with proptest, mocks with mockall, doc tests, and Criterion benchmarks. It also describes a RED-GREEN-REFACTOR TDD workflow, coverage measurement with cargo-llvm-cov and target thresholds, common cargo test commands, best practices, and a GitHub Actions CI example. It produces guidance and example code rather than executable tooling.
When to use it
Use it when adding or reviewing tests for Rust functions, methods or traits, when introducing TDD into a Rust project, when setting up property-based tests, mocks or benchmarks, or when configuring coverage thresholds and CI test steps.
Requirements
Requires a Rust toolchain with cargo; examples reference the crates tokio, rstest, proptest, mockall and criterion, and the cargo-llvm-cov tool for coverage. No scripts are shipped; the skill is instructions and code samples only.

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

rust
// RED: Write test first, use todo!() as placeholderpub 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'
rust
// GREEN: Replace todo!() with minimal implementationpub fn add(a: i32, b: i32) -> i32 { a + b }// cargo test → PASS, then REFACTOR while keeping tests green

単体テスト

モジュールレベルのテスト整理

rust
// src/user.rspub 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"));    }}

アサーションマクロ

rust
assert_eq!(2 + 2, 4);                                    // Equalityassert_ne!(2 + 2, 5);                                    // Inequalityassert!(vec![1, 2, 3].contains(&2));                     // Booleanassert_eq!(value, 42, "expected 42 but got {value}");    // Custom messageassert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON);   // Float comparison

エラーとパニックのテスト

Result の戻り値のテスト

rust
#[test]fn parse_returns_error_for_invalid_input() {    let result = parse_config("}{invalid");    assert!(result.is_err());
    // Assert specific error variant    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(()) // Test fails if any ? returns Err}

パニックのテスト

rust
#[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];}

統合テスト

ファイル構造

text
my_crate/├── src/│   └── lib.rs├── tests/              # 統合テスト│   ├── api_test.rs     # 各ファイルが独立したテストバイナリ│   ├── db_test.rs│   └── common/         # 共有テストユーティリティ│       └── mod.rs

統合テストの書き方

rust
// tests/api_test.rsuse 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の使用

rust
#[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 を使用したパラメータ化テスト

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

テストヘルパー関数

rust
#[cfg(test)]mod tests {    use super::*;
    /// Creates a test user with sensible defaults.    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 を使用したプロパティベーステスト

基本的なプロパティテスト

rust
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]);        }    }}

カスタムストラテジー

rust
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 を使用したモック

トレイトベースのモック

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

ドキュメントテスト

実行可能なドキュメント

rust
/// Adds two numbers together.////// # Examples////// ```/// 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}
/// Parses a config string.////// # Errors////// Returns `Err` if the input is not valid TOML.////// ```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を使用したベンチマーク

toml
# Cargo.toml[dev-dependencies]criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]name = "benchmark"harness = false
rust
// benches/benchmark.rsuse 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);

テストカバレッジ

カバレッジの実行

bash
# Install: cargo install cargo-llvm-cov (or use taiki-e/install-action in CI)cargo llvm-cov                    # Summarycargo llvm-cov --html             # HTML reportcargo llvm-cov --lcov > lcov.info # LCOV format for CIcargo llvm-cov --fail-under-lines 80  # Fail if below threshold

カバレッジ目標

コードの種類目標
クリティカルなビジネスロジック100%
パブリックAPI90%以上
汎用コード80%以上
生成済み / FFIバインディング除外

テストコマンド

bash
cargo test                        # Run all testscargo test -- --nocapture         # Show println outputcargo test test_name              # Run tests matching patterncargo test --lib                  # Unit tests onlycargo test --test api_test        # Integration tests onlycargo test --doc                  # Doc tests onlycargo test --no-fail-fast         # Don't stop on first failurecargo test -- --ignored           # Run ignored tests

ベストプラクティス

すべきこと:

  • まずテストを書く (TDD)
  • 単体テストには #[cfg(test)] モジュールを使用する
  • 実装ではなく動作をテストする
  • シナリオを説明する記述的なテスト名を使用する
  • より良いエラーメッセージのために assert! より assert_eq! を優先する
  • クリーンなエラー出力のために Result を返すテストでは ? を使用する
  • テストを独立させる——共有の可変状態なし

すべきでないこと:

  • Result::is_err() をテストできる場合に #[should_panic] を使用する
  • すべてをモックする——可能なら統合テストを優先する
  • フレーキーなテストを無視する——修正または分離する
  • テストで sleep() を使用する——チャンネル、バリア、または tokio::time::pause() を使用する
  • エラーパスのテストをスキップする

CI統合

yaml
# GitHub Actionstest:  runs-on: ubuntu-latest  steps:    - uses: actions/checkout@v4    - uses: dtolnay/rust-toolchain@stable      with:        components: clippy, rustfmt
    - name: Check formatting      run: cargo fmt --check
    - name: Clippy      run: cargo clippy -- -D warnings
    - name: Run tests      run: cargo test
    - uses: taiki-e/install-action@cargo-llvm-cov
    - name: Coverage      run: cargo llvm-cov --fail-under-lines 80

覚えておくこと:テストはドキュメントである。コードをどのように使うべきかを示している。明確に書き、最新の状態を保つこと。

Source and attribution

Source:affaan-m/ECCindocs/ja-JP/skills/rust-testingat commitef648e0

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal