🧵

【Rust】モックライブラリMockitoで非同期のテストコードを書く

2024/04/28に公開

はじめに

Mockitoは、RustでHTTPのモックサーバーを作るライブラリ(クレート)です。
Java用の同名のモックフレームワークMockitoとは別物です。。

APIなど外部サービスに接続する処理をユニットテスト実行時にもおこなうと、
APIの稼働状況や通信状況など、外部要因によりテスト結果が左右されることがあります。

しかし外部サービスと同様のレスポンスを定義したモックを利用することにより、
テスト時には外部へリクエスト送信せず、安定してテストを実行できるようになります。

当記事ではAPIデータを取得する非同期処理のテストで、Mockitoを使用した例をご紹介します。

https://crates.io/crates/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構造体にして返します。

コード全体は、以下を展開して確認ください。

サンプルコード全体
Cargo.toml
[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"
main.rs
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も試してみたいと思います。

コラボスタイル Developers

Discussion