mcpField
R
rust-api-test-harness

rust-api-test-harness

github.com/lvtd-llcテスト・品質163 回インストール7 週間前に更新
リポジトリ

rust-api-test-harness について

Rust API の公開 HTTP 動作を通じて、再現性があり CI に対応するテストを作るためのスキルです。Rust 開発者向けに、テスト起動、依存関係の分離、成功・失敗ケースの検証を案内します。

主な機能

  1. 01ブラックボックス HTTP 統合テスト
  2. 02再利用可能なアプリ起動とテストフィクスチャ
  3. 03データベースと外部 API の分離
  4. 04エラーと副作用のアサーション
  5. 05Cargo の事前検証

活用シーン

  1. 01Rust サービスに HTTP 統合テストを追加
  2. 02状態を分離してデータベース動作をテスト
  3. 03サードパーティ HTTP 依存関係をモック
  4. 04API エラーとリトライ関連の結果を確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/lvtd-llc/skills --skill rust-api-test-harness

インストール後、Claude Code で/rust-api-test-harnessを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
references/
database-test-isolation.md
external-http-mocks.md
scripts/
check-rust-service.sh

詳細

Rust API テストハーネス

このスキルを使うと、Rust API の開発をテストファーストで進め、再現可能かつ CI 対応にできます。 フレームワークの内部に踏み込むのではなく、HTTP 経由でサービスを利用し、観測可能な動作を検証するブラックボックステストを優先してください。

基本的なワークフロー

  1. まずプロジェクトを調べます。Cargo.toml、ワークスペースの構成、既存の src/main.rs、src/lib.rs、ルーター/起動モジュール、tests/、マイグレーション、Docker Compose、CI ワークフローを確認します。
  2. アプリケーションの境界を見つけます。必要に応じて起動コードを再利用可能な関数に切り出し、テストでランダムなローカルポートを割り当て、状態を明示的に渡せるようにします。
  3. 動作を変更する前に、統合テストを作成または更新します。プロジェクトに明確な慣例がある場合を除き、API テストは tests/ に配置します。
  4. spawn_app ヘルパーを作成し、各テストケースでサービスを 1 回起動して、ベース URL を取得し、破棄可能なテストリソースを管理します。
  5. プロジェクトが意図的にインプロセステストを採用している場合を除き、フレームワーク固有のリクエストオブジェクトではなく reqwest を使って、公開 HTTP コントラクトをテストします。
  6. 状態を分離します。動作が依存先のセマンティクスに左右される場合は、実際の依存先を使います。SQL の動作には Postgres、外部 API にはモック HTTP サーバーを使い、テストごとに一意のリソース名を割り当てます。
  7. テストヘルパーは本番品質のコードとして扱います。失敗時に役立つメッセージを表示し、小さなヘルパーメソッドで定型処理を隠し、準備完了を確認できる場合はスリープを避けます。
  8. 作業を終える前に、ローカルの検証スクリプトまたは同等の cargo コマンドを実行します。

起動時の構成

ハンドラーモジュール内でインフラストラクチャを作成するのではなく、インフラストラクチャを受け取る起動関数を優先します。Actix は同期型の std::net::TcpListener でリッスンできます。

pub fn run(
    listener: std::net::TcpListener,
    app_state: AppState,
) -> Result<actix_web::dev::Server, std::io::Error> {
    let state = actix_web::web::Data::new(app_state);
    let server = actix_web::HttpServer::new(move || {
        actix_web::App::new()
            .app_data(state.clone())
            .route("/health_check", actix_web::web::get().to(health_check))
    })
    .listen(listener)?
    .run();

    Ok(server)
}

Axum では同じ境界を優先しつつ、Tokio の非同期リスナー型を使います。

pub fn router(app_state: AppState) -> axum::Router {
    axum::Router::new()
        .route("/health_check", axum::routing::get(health_check))
        .with_state(app_state)
}

pub async fn spawn_axum_app() -> TestApp {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
        .await
        .expect("failed to bind random port");
    let port = listener.local_addr().unwrap().port();
    let server = axum::serve(listener, router(test_state().await));
    let server_handle = tokio::spawn(async move { server.await });

    TestApp {
        address: format!("http://127.0.0.1:{port}"),
        http_client: reqwest::Client::new(),
        server_handle,
    }
}

テストハーネスのパターン

各テストでセットアップを繰り返すのではなく、ヘルパーオブジェクトを使います。

pub struct TestApp {
    pub address: String,
    pub http_client: reqwest::Client,
    pub server_handle: tokio::task::JoinHandle<std::io::Result<()>>,
}

pub async fn spawn_app() -> TestApp {
    let listener = std::net::TcpListener::bind("127.0.0.1:0")
        .expect("failed to bind random port");
    let port = listener.local_addr().unwrap().port();

    let server = my_app::startup::run(listener, test_state().await)
        .expect("failed to start test server");
    let server_handle = tokio::spawn(server);

    TestApp {
        address: format!("http://127.0.0.1:{port}"),
        http_client: reqwest::Client::new(),
        server_handle,
    }
}

複数のテストで複数ステップのユーザージャーニーが登場する場合は、小さなヘルパーメソッドを追加します。post_subscription、login、create_newsletter、confirm_subscription のように、動作を表すメソッド名にしてください。

tokio::spawn が返す JoinHandle を破棄しないでください。テストフィクスチャに保持しておけば、サーバータスクが予期せず終了したことをテストで検出し、後続の接続拒否エラーの陰にサーバー側のパニックを隠すのではなく、終了処理中にタスクを中断できます。

アサーション

  • まずステータスコードとレスポンスヘッダーを検証します。
  • 可能であれば公開 API を通じて永続化された副作用を検証します。公開 API にまだ観測手段がない場合に限り、データベースを直接クエリします。
  • 否定的なケースも第一級の動作として検証します。必須フィールドの欠落、不正なペイロード、重複送信、未認証ユーザー、上流サービスの障害、タイムアウトなどが対象です。
  • API コントラクトにそのままのバイト列が含まれる場合を除き、ノイズの多いレスポンスボディのスナップショットは避けます。

状態と依存先の分離

テストでデータベースを使用する場合は、references/database-test-isolation.md を読んでください。 テストでサードパーティの HTTP API を呼び出す場合は、references/external-http-mocks.md を読んでください。

標準の方針:

  • Postgres に依存する動作には、SQLite やインメモリの代替手段ではなく、実際の Postgres を使います。
  • テストで共有状態に書き込む可能性がある場合は、テストごとに論理データベース、スキーマ、または一意の名前空間を使います。
  • アプリケーションの起動前に、セットアップ中にマイグレーションを実行します。
  • 外部 HTTP API には wiremock または同等のローカルモックサーバーを使います。
  • 失敗時にテストが停止しなくなるのを防ぐため、テストではタイムアウトを短めに設定します。

検証

標準的な cargo ワークフローを適用できる場合は、Rust プロジェクトのルートから同梱の preflight スクリプトを実行します。

path/to/rust-api-test-harness/scripts/check-rust-service.sh

デフォルトでは、スクリプトは以下を実行します。

cargo fmt --all --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features

RUST_API_TEST_HARNESS_SKIP_CLIPPY=1 を設定すると、clippy の手順のみをスキップします。スクリプトは引き続きフォーマットとテストを実行します。プロジェクトで CI の lint を別途設定している場合や、--all-targets --all-features がローカルの preflight には適さないと分かっている場合は、この回避策を使用してください。

CI でプロジェクトのすべての機能を使用していない場合は、代わりにリポジトリで既に使われている CI コマンドに合わせ、異なる手順にした理由を説明してください。

参考ファイル

  • references/database-test-isolation.md:Postgres を利用するテスト、マイグレーション、テストごとのデータベース、クリーンアップのトレードオフに関するパターンです。
  • references/external-http-mocks.md:外部向け HTTP クライアントと API ワークフローをローカルモックでテストするパターンです。

コメント