0Pricing
Learn Rust Coding · レッスン

APIのテスト

統合テスト

「APIのテスト」はCoddyKit上の無料Learn Rust Codingレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLearn Rust Coding学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Learn Rust Codingコースには全4レッスンが含まれています。

APIをテストする理由

テストによって、エンドポイントが正しく動作し、コードを変更しても動作し続けるという確信を得られます。REST APIで最も価値の高いテストは統合テストです。統合テストでは実際のルーターをエンドツーエンドで動かし、リクエストを送信してレスポンスを検証します。

このレッスンでは、ユニットテスト、Towerのoneshotテクニック、完全な統合テストについて扱います。

純粋なロジックのユニットテスト

バリデーションなど、ネットワークに触れないロジックは、通常のRustユニットテストでテストできます。コードの隣に#[cfg(test)]モジュールを配置します。これらのテストはcargo testですばやく実行できます。

fn validate_title(title: &str) -> bool {
    !title.trim().is_empty()
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn rejects_empty() {
        assert!(!validate_title("   "));
        assert!(validate_title("buy milk"));
    }
}

非同期テスト

ハンドラーは非同期なので、awaitするテスト関数はランタイム上で実行する必要があります。#[test]の代わりに#[tokio::test]を使用します。これにより、そのテスト用のTokioランタイムが起動し、.awaitで非同期コードを呼び出せます。

async fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;
    #[tokio::test]
    async fn adds() {
        assert_eq!(add(2, 3).await, 5);
    }
}

ネットワークを使わないテスト

AxumのルーターはTowerのServiceトレイトを実装しているため、ポートをバインドせずにリクエストを直接渡せます。oneshotメソッドは1つのRequestを受け取り、Responseを返します。これにより、統合テストを高速かつ決定的に実行できます。

// dev-dependencies needed: tower (for ServiceExt), http-body-util
use axum::{Router, routing::get};
use axum::http::{Request, StatusCode};
use axum::body::Body;
use tower::ServiceExt; // brings in oneshot

async fn check() {
    let app = Router::new().route("/health", get(|| async { "ok" }));
    let res = app
        .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
        .await.unwrap();
    assert_eq!(res.status(), StatusCode::OK);
}

ステータスコードの検証

ほとんどのテストで最初に確認するのはHTTPステータスです。存在しないリソースには404、作成に成功した場合は201、不正なボディには400が返されるはずです。ルート向けのリクエストを作成し、res.status()を期待するコードと比較します。

use axum::http::{Request, StatusCode};
use axum::body::Body;
use tower::ServiceExt;

async fn missing_returns_404(app: axum::Router) {
    let res = app
        .oneshot(Request::builder()
            .uri("/todos/999")
            .body(Body::empty()).unwrap())
        .await.unwrap();
    assert_eq!(res.status(), StatusCode::NOT_FOUND);
}

レスポンスボディの読み取り

JSONを検証するには、レスポンスボディをバイト列に集めてデシリアライズします。http_body_util::BodyExt::collectヘルパーでボディを集め、serde_jsonでモデルにパースしてフィールド単位で検証します。

use http_body_util::BodyExt;

async fn read_json(res: axum::http::Response<axum::body::Body>) {
    let bytes = res.into_body().collect().await.unwrap().to_bytes();
    let todo: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
    assert_eq!(todo["done"], false);
}

JSONボディの送信

POSTをテストするには、JSONボディと正しいContent-Typeヘッダーを持つリクエストを作成します。入力構造体をシリアライズし、content-type: application/jsonを設定して、oneshotに渡します。

use axum::http::{Request, StatusCode, header};
use axum::body::Body;
use tower::ServiceExt;

async fn create_returns_201(app: axum::Router) {
    let body = serde_json::json!({ "title": "test" }).to_string();
    let res = app
        .oneshot(Request::builder()
            .method("POST").uri("/todos")
            .header(header::CONTENT_TYPE, "application/json")
            .body(Body::from(body)).unwrap())
        .await.unwrap();
    assert_eq!(res.status(), StatusCode::CREATED);
}

再利用可能なテストアプリビルダー

各テストはクリーンな状態から開始する必要があります。新しいインメモリデータベースまたはテストデータベースを使って、新しいルーターを構築するヘルパーを作成します。各テストでこれを呼び出すとテストが分離され、あるテストが別のテストに影響を与えなくなります。

use axum::Router;
use std::sync::{Arc, Mutex};

fn test_app() -> Router {
    let store = Arc::new(Mutex::new(Vec::new()));
    build(store) // same build() the real server uses
}

testsディレクトリ

統合テストはトップレベルのtests/フォルダーに配置します。そこにある各ファイルは、ライブラリの公開APIを使用する別個のクレートとしてコンパイルされます。これにより、実際のユーザーが目にするものと同じインターフェースを通してテストすることになります。

  • tests/api.rsにエンドポイントのテストを記述します。
  • cargo testですべて実行します。
// tests/api.rs
use my_api::build_router; // exported from lib.rs

#[tokio::test]
async fn health_ok() {
    let _app = build_router();
    // send a request and assert ...
}

データベースに対するテスト

ハンドラーがsqlxを使用する場合、テストにもデータベースが必要です。一般的な方法には、専用のテストデータベース、各テスト後にロールバックするトランザクション、テストごとにクリーンなデータベースを自動的に用意するsqlxの#[sqlx::test]マクロがあります。

// requires sqlx test features and a DATABASE_URL
use sqlx::PgPool;

#[sqlx::test]
async fn inserts_todo(pool: PgPool) {
    let todo = create(&pool, "learn rust").await.unwrap();
    assert_eq!(todo.title, "learn rust");
    assert_eq!(todo.done, false);
}

何をテストするか

バランスの取れたテストスイートを目指します。

  • 正常系:有効なリクエストが正しいステータスとボディを返すこと。
  • エラー:存在しないリソースには404、不正な入力には400が返ること。
  • エッジケース:空のリスト、境界値、重複など。

CIでcargo testを実行し、デプロイ前にリグレッションを検出できるようにします。

簡単な確認

APIのテストについて理解度を確認しましょう。

まとめ

RustのREST APIをテストする方法を学びました。

  • 純粋なロジックはユニットテストし、非同期コードには#[tokio::test]を使用します。
  • oneshotでルーターを直接動かせるため、ポートは必要ありません。
  • ボディとヘッダーを持つリクエストを作成し、レスポンスボディを集めてパースします。
  • テストごとに新しいアプリビルダーを使用して分離し、統合テストをtests/に配置します。
  • #[sqlx::test]でクリーンなデータベースを用意し、正常系、エラー、エッジケースを網羅します。

よくある質問

「APIのテスト」レッスンは無料ですか?

はい。「APIのテスト」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Learn Rust Codingコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Learn Rust Codingコースには全4レッスンが含まれています。

「APIのテスト」で何を学びますか?

統合テスト ブラウザで直接実行するハンズオンコードでLearn Rust Codingを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Learn Rust Codingを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのLearn Rust Codingは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「APIのテスト」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このLearn Rust Codingレッスンでコードを書いて実行できますか?

はい。すべてのLearn Rust Codingレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. プロジェクトのセットアップ
  2. エンドポイントとモデル
  3. データベース統合
  4. APIのテスト
← Learn Rust Codingに戻る