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フィードバックを取得できます。ローカル設定は不要です。