🧪

axum crate のテスト

に公開
2025/12/29

前回は axum crate の Router::fallback について書きました。

今回は axum crate のテストについて書きます。

今回も axum crate のバージョンは 0.8.6 です。

axum crate の examples

axum の GitHub リポジトリの examples には testing があります。

https://github.com/tokio-rs/axum/tree/axum-v0.8.6/examples/testing

Cargo.toml と src/main.rs の 2 ファイルです。

src/main.rs からテストのひとつ hello_world の部分を引用します。

use super::*;
use axum::{
    body::Body,
    extract::connect_info::MockConnectInfo,
    http::{self, Request, StatusCode},
};
use http_body_util::BodyExt; // for `collect`
use serde_json::{json, Value};
use tokio::net::TcpListener;
use tower::{Service, ServiceExt}; // for `call`, `oneshot`, and `ready`

#[tokio::test]
async fn hello_world() {
    let app = app();

    // `Router` implements `tower::Service<Request<Body>>` so we can
    // call it like any tower service, no need to run an HTTP server.
    let response = app
        .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
        .await
        .unwrap();

    assert_eq!(response.status(), StatusCode::OK);

    let body = response.into_body().collect().await.unwrap().to_bytes();
    assert_eq!(&body[..], b"Hello, World!");
}

コードとコメントで十分に説明されていますが、改めて書いていきます。

app は Router を返しています。

コメントにあるとおり axum の Router は tower::Service を実装しており、 tower::Service をテストするのと変わらず、 HTTP サーバーを動かす必要はありません。

tower::Service が何であるかは middleware を取り上げるときに書くと思うのですが、雑には tower-service crate の README の一文 async fn(Request) -> Result<Response, Error> が分かりやすいです。 Request を引数に取り Result<Response, Error> を返す非同期関数のようなものです。

要するに Request を渡して、返される Result<Response, Error> をテストすればいいだけです。

tower crate は util feature を有効にすることで ServiceExt trait を使用できます。ここには oneshot メソッドがあり、単発のリクエストを処理できます。

これを使うと hello_world のような形になります。

リクエストボディを取り出す箇所がすこしややこしいです。型パラメーターなしの collect に unwrap に to_bytes で何がなにやらですね。

let body = response.into_body().collect().await.unwrap().to_bytes();
assert_eq!(&body[..], b"Hello, World!");

axum::response::Response::into_body は Response<T> の T を返します。既定値は axum::body::Body です。

axum::body::Body::collect は http_body_util::combinators::Collect を返します。 Collect は Future を実装しており、 Result<Collected<<T as Body>::Data>, <T as Body>::Error> を返します。

http_body_util::Collected::to_bytes は bytes::Bytes を返します。

impl AsRef<[u8]> for bytes::Bytes があるので、 b"Hello, World!" と比較できるわけですね。

もう途中から他のクレートの話ですね。

個人的な使い方

ぼくは前回の例でも使用していたのですが、簡単なユーティリティを書いて使うことが多いです。

こういうユーティリティです。

async fn send_request(
    router: axum::Router<()>,
    request: axum::http::Request<axum::body::Body>,
) -> anyhow::Result<axum::response::Response<axum::body::Body>> {
    let response = tower::ServiceExt::oneshot(router, request).await?;
    Ok(response)
}

trait ResponseExt {
    async fn into_body_string(self) -> anyhow::Result<String>;
}

impl ResponseExt for axum::response::Response<axum::body::Body> {
    async fn into_body_string(self) -> anyhow::Result<String> {
        let bytes = axum::body::to_bytes(self.into_body(), usize::MAX).await?;
        Ok(String::from_utf8(bytes.to_vec())?)
    }
}

こういう感じで使います。

#[tokio::test]
async fn test_path_not_found() -> anyhow::Result<()> {
    let router = router();
    let request = axum::http::Request::builder()
        .method(axum::http::Method::GET)
        .uri("/unknown")
        .body(axum::body::Body::empty())?;
    let response = send_request(router, request).await?;
    assert_eq!(response.status(), axum::http::StatusCode::OK);
    assert_eq!(response.into_body_string().await?, "fallback");
    Ok(())
}

send_request は tower::ServiceExt::oneshot を wrap したもので、型を固定して扱いやすくしています。

into_body_string はレスポンスボディを文字列にして返すものです。

業務ではさらにファイルから読み込んで結果と比較したり……みたいなことをしますが、最低これくらいを用意するだけでも個人的には扱いやすいです。 unwrap やら collect やら読みづらくて仕方ないので。

axum は 0.7 から Body が hyper::Body から axum 固有の struct になって扱いやすくなったように思います (破壊的変更はアレでしたけど……) 。

axum::body::to_bytes は axum::body::Body から bytes::Bytes に変換するものです。

response.into_body().collect().await.unwrap().to_bytes() よりは axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap() のほうが読みやすくないですか? 好みの問題ですかね……。

おわり

今回は axum crate の examples から testing をながめつつ、個人的にテストで使用しているユーティリティを紹介しました。

次回はおそらく axum crate の Router の merge や nest について書きます。

GitHubで編集を提案
ドクターメイト

Discussion