【Rust】モックライブラリMockitoで非同期のテストコードを書く
はじめに
Mockitoは、RustでHTTPのモックサーバーを作るライブラリ(クレート)です。
Java用の同名のモックフレームワークMockitoとは別物です。。
APIなど外部サービスに接続する処理をユニットテスト実行時にもおこなうと、
APIの稼働状況や通信状況など、外部要因によりテスト結果が左右されることがあります。
しかし外部サービスと同様のレスポンスを定義したモックを利用することにより、
テスト時には外部へリクエスト送信せず、安定してテストを実行できるようになります。
当記事ではAPIデータを取得する非同期処理のテストで、Mockitoを使用した例をご紹介します。
テスト対象のコード
テストやプロトタイピングで使えるAPIとして公開されているJSONPlaceholderから、
RustでToDoデータを取得する処理を、今回のテスト対象とします。
以下のfetch_todo_api()
が、ToDoを取得する非同期関数のサンプルコードです。
async fn fetch_todo_api(url: &str) -> Result<Todo, Error> {
let response = reqwest::get(url).await?;
response.json::<Todo>().await
}
エンドポイントのURLは、テスト時にモック用URLに差し替えられるよう、引数にしました。
reqwestライブラリを使用して外部APIにリクエストを送信し、
JSONで受け取ったToDoデータを、ToDo構造体にして返します。
コード全体は、以下を展開して確認ください。
サンプルコード全体
[dependencies]
tokio = { version = "1.37", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
mockito = "1.4"
use reqwest::Error;
use serde::Deserialize;
#[derive(Deserialize)]
struct Todo {
#[serde(rename = "userId")]
user_id: u32,
id: u32,
title: String,
completed: bool,
}
async fn fetch_todo_api(url: &str) -> Result<Todo, Error> {
let response = reqwest::get(url).await?;
response.json::<Todo>().await
}
fn todo_details(todo: &Todo) -> String {
format!(
"Todo:\nUserId: {}\nId: {}\nTitle: {}\nCompleted: {}",
todo.user_id, todo.id, todo.title, todo.completed
)
}
#[tokio::main]
async fn main() {
if let Some(todo_id) = std::env::args().nth(1) {
let base_url = "https://jsonplaceholder.typicode.com/todos/";
let url = format!("{}{}", base_url, todo_id);
match fetch_todo_api(&url).await {
Ok(todo) => println!("{}", todo_details(&todo)),
Err(err) => eprintln!("Error: {}", err),
}
} else {
eprintln!("Error: Todo ID not provided");
}
}
以下が実行例で、ToDoのIDを引数で指定して取得処理を実行します。
% cargo run 123
Todo:
UserId: 7
Id: 123
Title: esse et quis iste est earum aut impedit
Completed: false
テストコード
Rustのテストコードの基本的な書き方は、以下記事などを参照ください。
以下が今回作成したfetch_todo_api()
のテストコードです。
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_fetch_todo_api() {
let mut server = mockito::Server::new_async().await;
let path = "/todos/1";
let json_body = r#"{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}"#;
let mock = server.mock("GET", path)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json_body)
.create_async().await;
let url = server.url() + path;
let todo: Todo = fetch_todo_api(&url).await.unwrap();
assert_eq!(todo.user_id, 1);
assert_eq!(todo.id, 1);
assert_eq!(todo.title, "delectus aut autem");
assert!(!todo.completed);
mock.assert_async().await;
}
}
解説1:テストコードの非同期化
fetch_todo_api()
が非同期関数のため、
テストコードも#[tokio::test]
、async fn
で非同期関数として定義します。
解説2:モックサーバーの作成
mockito::Server
を使用してモックサーバーを作成し、ToDoのAPIの応答を模倣します。
new_async()
メソッドで非同期のモックサーバーを作成し、
mock()
メソッドを使用してモックリクエストを定義します。
リクエストのメソッド、パス、ステータスコード、レスポンスボディなどを設定し、
モックリクエストも非同期用のcreate_async()
メソッドで作成しています。
解説3:モックサーバーのURL取得
モックのURLをserver.url() + path
で取得し、fetch_todo_api()
に引数で渡します。
解説4:応答の確認
assert_eq!
やassert!
を使用して、取得した値が期待通りか確認します。
これによりテスト対象の関数が、正しくレスポンスを取得しているか確認できます。
mock.assert_async().await;
では、非同期のモックが実行されたかを確認しています。
テスト実行と結果
cargo test
でテストを実行し、okと表示されたら成功です!🎊
% cargo test
running 1 test
test tests::test_fetch_todo_api ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
もし、FAILEDと表示されたら失敗です😥
おわりに
Mockitoを使うことで、外部サービスのモック化やレスポンスの定義が簡単にできました。
モック化により外部依存する要素を排除することで、テストの安定性も増しますし、
外部との通信がないためテストの実行時間も短縮できます♪
今回のサンプルはGETでの利用でしたが、POSTやPUTも試してみたいと思います。
Discussion