axum crate のテスト
前回は 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 について書きます。
Discussion