DEV Community

Cover image for Rust API テスト方法
Akira
Akira

Posted on • Originally published at apidog.com

Rust API テスト方法

Rustでは、数百行で高速かつ型安全なHTTPサーバーを実装できます。一方で、API契約の検証までRustツールチェインだけで回すと、cargo test の再実行、統合テストの保守、フロントエンド向けモックの二重実装がボトルネックになります。APIとして出荷するなら、実行中のRustサーバーに対してHTTPレベルで検証できるフィードバックループを用意しておくべきです。

今すぐApidogを試す

このガイドでは、Apidog を使ってRust APIをテストする実装手順をまとめます。AxumまたはActix-webのサーバーをApidogに接続し、リクエスト作成、Serde JSONの検証、JWT認証、未完成エンドポイントのモック化、CIでの契約テスト実行までを一通り構築します。

Postmanやcurl中心の運用から移行する場合でも、Apidogでは保存済みリクエストからOpenAPI仕様を生成し、共有可能なモックURLやチーム環境を使えます。Postmanからの移行に関する内容 は別記事に譲り、ここではRust APIのテストに集中します。

TL;DR

  • Rustサーバーをローカルで起動し、Apidog環境に http://localhost:3000baseUrl として登録する。
  • GET /healthz を最初のスモークテストとして保存する。
  • Serde構造体に対応するJSONリクエストを作り、レスポンス形状をテストスクリプトで検証する。
  • JWTはプリリクエストスクリプトで発行し、{{token}} をフォルダレベルのBearer認証に設定する。
  • 未完成のRustハンドラーはApidogのモックで先に契約を公開する。
  • テストシナリオとして保存し、apidog-cli でCIから実行する。

Rustツールチェイン外でRust APIをテストする理由

cargo test はRustコードの検証には有効です。ただし、HTTP APIの契約を確認するには次のような課題があります。

  • コンパイルとテスト実行が重い
  • Rust以外のメンバーがテスト内容を確認しづらい
  • ステータスコード、ヘッダー、JSON形状、エラー形式をHTTP視点で管理しにくい
  • フロントエンド向けモックを別途実装しがち

たとえばAxumでハンドラーを直接テストする場合、tower::ServiceExt::oneshot を使った統合テストを複数書くことになります。さらにフロントエンドが同じレスポンスを使いたい場合、別途JavaScriptやモックサーバーで再実装することもあります。

Apidogを使うと、実行中のRustバイナリに対してHTTP契約を1か所で管理できます。

主なメリットは次の3つです。

  1. 契約チェックをビルドから切り離せる

    Apidogは実行中のサーバーにHTTPリクエストを送ります。rustc の完了を待たずに、APIレスポンスの形状を確認できます。

  2. モックをチームで共有できる

    未完成のハンドラーでも、合意済みのJSONを返すURLをフロントエンドに渡せます。

  3. 保存済みリクエストからOpenAPIを生成できる

    すべてのルートに utoipaaide のアノテーションを書く前でも、Apidog上のリクエストからOpenAPI 3.1ドキュメントを出力できます。

ステップ1:RustサーバーをApidog環境として追加する

まずRust APIを起動します。Axumの最小構成は次のとおりです。

use axum::{routing::get, Router};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() {
    let app = Router::new().route("/healthz", get(|| async { "ok" }));

    let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();

    axum::serve(listener, app).await.unwrap();
}
Enter fullscreen mode Exit fullscreen mode

Apidogで新しいプロジェクトを作成し、環境管理から Rust Local 環境を追加します。

変数
baseUrl http://localhost:3000
token 空のまま
apiVersion v1

ステージング環境がある場合は、別途 Rust Staging を作り、デプロイ済みのベースURLを設定します。以後、リクエストURLには {{baseUrl}} を使えば、環境の切り替えだけでローカルとステージングを行き来できます。

ステップ2:最初のエンドポイントを叩く

Apidog内に Rust API フォルダを作成し、新しいリクエストを追加します。

  • メソッド: GET
  • URL: {{baseUrl}}/healthz

送信して、次のレスポンスを確認します。

  • ステータス: 200
  • ボディ: ok

このリクエストを health-check として保存します。以後のテストシナリオでは、最初にこのリクエストを実行して、サーバー起動と環境変数が正しいことを確認します。

接続拒否になる場合は、次を確認してください。

  • Rustサーバーが起動しているか
  • ポートが 3000 で合っているか
  • 127.0.0.1 ではなく 0.0.0.0 にバインドしているか

ローカルのApidogやDockerコンテナからアクセスする場合、TcpListener::bind("0.0.0.0:3000") にしておくと接続トラブルを避けやすくなります。

ステップ3:Serde JSONリクエストとレスポンスをテストする

次に、JSONを受け取りJSONを返す POST /users を追加します。

use axum::{extract::Json, routing::post, Router};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}

async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
    Json(User {
        id: 1,
        name: payload.name,
        email: payload.email,
    })
}

let app = Router::new().route("/users", post(create_user));
Enter fullscreen mode Exit fullscreen mode

Apidogでリクエストを作成します。

  • メソッド: POST
  • URL: {{baseUrl}}/users
  • Body: JSON
{
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}
Enter fullscreen mode Exit fullscreen mode

送信し、User JSONが返ることを確認したら、create-user として保存します。

次に「テスト」タブでレスポンスを検証します。

pm.test("Status is 200", () => {
  pm.expect(pm.response.code).to.eql(200);
});

pm.test("Body has id, name, email", () => {
  const body = pm.response.json();

  pm.expect(body).to.have.property("id");
  pm.expect(body.name).to.eql("Ada Lovelace");
  pm.expect(body.email).to.match(/^[^@]+@[^@]+$/);
});
Enter fullscreen mode Exit fullscreen mode

このテストにより、Rust側でSerde属性を変更してレスポンス形状が変わった場合、デプロイ前に契約の破壊を検出できます。

ステップ4:Serdeのリジェクションケースをカバーする

正常系だけでなく、不正な入力もApidogに保存しておきます。Axumでは、JSON抽出に失敗した場合、デフォルトで 422 Unprocessable Entity が返ります。

次の3つのリクエストを作成します。

リクエスト ボディ 期待結果
create-user-missing-email { "name": "Ada" } 422、ボディに missing field email の記述
create-user-extra-field { "name": "Ada", "email": "a@b.c", "admin": true } #[serde(deny_unknown_fields)] がない場合は 200、ある場合は 422
create-user-wrong-type { "name": 1, "email": "a@b.c" } 422invalid type: integer の記述

各リクエストにステータスコードのアサーションを追加します。

例:

pm.test("Status is 422", () => {
  pm.expect(pm.response.code).to.eql(422);
});
Enter fullscreen mode Exit fullscreen mode

これにより、入力バリデーションの仕様をApidog上に明文化できます。後から deny_unknown_fields を有効にした場合も、公開契約の変更としてテスト失敗で検出できます。

ステップ5:JWTで保護されたルートをテストする

多くのRust APIでは、認証ミドルウェアの背後にハンドラーがあります。たとえば、Cookie内のJWTを検証するAxumハンドラーは次のようになります。

use axum::{Json, http::StatusCode};
use axum_extra::extract::cookie::PrivateCookieJar;
use jsonwebtoken::{decode, DecodingKey, Validation};

async fn me(jar: PrivateCookieJar) -> Result<Json<User>, StatusCode> {
    let token = jar.get("token").ok_or(StatusCode::UNAUTHORIZED)?;

    let claims = decode::<Claims>(
        token.value(),
        &DecodingKey::from_secret(b"secret"),
        &Validation::default(),
    )
    .map_err(|_| StatusCode::UNAUTHORIZED)?;

    Ok(Json(User {
        id: claims.claims.sub,
        name: "Ada".into(),
        email: "ada@example.com".into(),
    }))
}
Enter fullscreen mode Exit fullscreen mode

Apidogでは、JWTを毎回手作業で作る必要はありません。フォルダにプリリクエストスクリプトを設定します。

const jwt = require("jsonwebtoken");

const token = jwt.sign(
  {
    sub: 1,
    exp: Math.floor(Date.now() / 1000) + 3600
  },
  "secret"
);

pm.environment.set("token", token);
Enter fullscreen mode Exit fullscreen mode

次にフォルダ設定で認証を設定します。

  • 認証タイプ: Bearer Token
  • 値: {{token}}

これで、フォルダ内のすべてのリクエストが実行前に新しいJWTを生成し、Bearerトークンとして送信します。

JWT認証のテスト手順をより詳しく確認したい場合は、APIでのJWT認証をテストする方法 も参照してください。

ステップ6:ストリーミングとServer-Sent Eventsをテストする

RustのWebフレームワークはストリーミングレスポンスも扱えます。Axumの Ssefutures::Stream をラップし、text/event-stream としてチャンクを返します。

SSEのワイヤフォーマットは、一般的に次のような形式です。

data: { ... }

Enter fullscreen mode Exit fullscreen mode

Apidogでは通常のGETリクエストとしてSSEエンドポイントを作成できます。レスポンスの Content-Typetext/event-stream の場合、レスポンスパネルでストリーミング内容を確認できます。

確認すべきポイントは次のとおりです。

  • 最初のチャンクが期待時間内に到着するか
  • event: done のような完了イベントが発行されるか
  • ストリームが無限に続かないか
  • リクエスト設定でタイムアウトを設定し、完了しないストリームを失敗扱いにできるか

WebSocketを使っている場合も、ApidogのWebSocketリクエストタイプを使って接続、メッセージ送信、レスポンス検証を保存できます。

ステップ7:並行フロントエンド開発のためにRust APIをモック化する

フロントエンド開発は、Rustのコンパイル時間よりも「まだハンドラーが存在しない」ことにブロックされがちです。Apidogのモックを使えば、Rust側の実装前に合意済みレスポンスを返すURLを共有できます。

create-user リクエストを右クリックし、「スマートモック」を有効にします。Apidogは次のようなモックURLで User レスポンスを返します。

https://mock.apidog.com/m1/<projectId>/users
Enter fullscreen mode Exit fullscreen mode

保存済みの例に基づいたレスポンスが返るため、フロントエンドは実際のRustサーバーと同じように POST できます。

動的なレスポンスが必要な場合は、「高度なモック」でスクリプトを書きます。

return {
  id: Math.floor(Math.random() * 10000),
  name: body.name,
  email: body.email,
  createdAt: new Date().toISOString()
};
Enter fullscreen mode Exit fullscreen mode

このモックは、リクエストボディの nameemail を使い、生成した idcreatedAt を返します。

Rustハンドラーが完成したら、フロントエンドはベースURLを http://localhost:3000 に戻すだけです。考え方は他のランタイムでも同じで、Spring Boot APIの構築とテスト一般的なAPIテストワークフロー でも同様のパターンを使えます。

ステップ8:CIテストシナリオとして保存する

Apidogのテストシナリオでは、複数のリクエストを順番に実行し、変数を共有できます。Rust API用に次のシナリオを作成します。

  1. health-check を実行し、200 をアサートする。
  2. create-user を実行し、200 をアサートし、body.id を変数に保存する。
  3. create-user-missing-email を実行し、422 をアサートする。
  4. me をJWTプリリクエスト付きで実行し、200 をアサートする。
  5. SSEリクエストを実行し、5秒以内に完了することを確認する。

シナリオをJSONとしてエクスポートし、リポジトリに保存します。

tests/apidog/contract.json
Enter fullscreen mode Exit fullscreen mode

CIではRustサーバーを起動してから、apidog-cli で契約テストを実行します。

- name: Run API contract tests
  run: |
    cargo build --release
    ./target/release/myserver &
    sleep 2
    apidog-cli run tests/apidog/contract.json --env "Rust Local"
Enter fullscreen mode Exit fullscreen mode

これで、ハンドラーに変更を加えるPRは、ライブのRustバイナリに対してAPI契約テストを実行するようになります。Serdeのリネーム、ステータスコードの変更、JWT検証ロジックの変更などで公開契約が壊れた場合、マージ前に検出できます。

ステップ9:保存済みリクエストからOpenAPIを生成する

リクエストセットが安定したら、ApidogのエクスポートメニューからOpenAPI 3.1を選択します。保存済みリクエストと送信ボディの例をもとに、仕様ドキュメントを生成できます。

生成したOpenAPIは、次の用途に使えます。

  • TypeScriptクライアント生成
  • Swift / Kotlin / Pythonクライアント生成
  • フロントエンドとの契約共有
  • 外部パートナー向けAPI仕様書

Rustリポジトリに仕様を含めたい場合は、CIから apidog-cli export を実行し、openapi.json として保存します。

apidog-cli export --format openapi31 --output openapi.json
Enter fullscreen mode Exit fullscreen mode

これにより、コードとは別に、API利用者が参照できる最新の契約ファイルを管理できます。

FAQ

ApidogはAxumとActix-webの両方で動作しますか?

はい。ApidogはHTTPを扱うため、Rustフレームワークには依存しません。Axum、Actix-web、Rocket、Warp、Poem、Locoなど、HTTPリクエストに応答するサーバーであれば同じように使えます。

ローカルテストでは、必要に応じて 127.0.0.1 ではなく 0.0.0.0 にバインドしてください。

パニックを起こすハンドラーはどうテストしますか?

tower-httpCatchPanicLayer をルーターの前に入れると、パニックを 500 レスポンスに変換できます。そのうえで、パニックパスをトリガーするApidogリクエストを作成し、500 をアサートします。

ラップしない場合、接続が切断され、Apidogはネットワークエラーとして表示します。これもAPI契約として扱えます。

Docker内のRustバイナリに対してApidogを実行できますか?

はい。baseUrl をコンテナの公開ポートに向ければ実行できます。

Docker Compose内で実行している場合は、Apidogランナーを同じネットワークに置くか、ホストにマッピングされたポートを使います。

gRPCはテストできますか?

はい。ApidogにはgRPCリクエストタイプがあります。.proto ファイルをインポートし、サービスとメソッドを選択して、リクエストペイロードを送信できます。

認証、環境変数、テストシナリオの考え方はREST APIと同じです。

Apidogのテストシナリオは cargo test を置き換えますか?

いいえ。Rustコードの単体テストはRust内に残すべきです。

Apidogは、実行中のHTTP APIの契約を検証します。両者は検出するバグが異なります。

  • cargo test: 関数、ドメインロジック、型レベルの検証
  • Apidog: レスポンス形状、ステータスコード、ヘッダー、CORS、認証、エラー形式の検証

両方を使うのが安全です。

ApidogはRustのオープンソースプロジェクトで無料ですか?

はい。Apidogクライアントは個人および小規模チーム向けに無料で利用できます。テストシナリオ、モック、OpenAPIエクスポートも無料ティアに含まれます。

まとめ

Rust APIには、コンパイラーを待たずにHTTP契約を検証できるフィードバックループが必要です。Apidogを使うと、実行中のAxumまたはActix-webサーバーに対して、リクエスト、アサーション、モック、CIシナリオを1つのワークフローで管理できます。

まずは GET /healthzPOST /users、認証付きルート、エラーケースをApidogに保存してください。その時点で、APIの変更は「実行時の予期せぬ破壊」ではなく、「CIで検出できる契約差分」になります。

Apidogをダウンロード し、Rustサーバーの baseUrl を設定してください。10分程度で、cargo から切り離されたAPI契約テストと、フロントエンドに共有できるモック環境を用意できます。

Top comments (0)