🐥

RustでMockitoでAPIのモックをするときにbuilderを使う

に公開

目的

外部APIを含むテストを書いていると、単体テストで呼び出すわけにはいかにのでMcokを使います。特にHTTPのモックにはMockitoが便利です。

ですが、Mockitoを使って毎回モックを準備するのも大変です。そこでbuilderを使って楽をすることにします。

LINEログインのプロフィールAPIのモックをしてみます。リファレンスはこちらです。

またAPIの向き先を簡単に変えられるように、lineapiクレートを使います。

コード

普通にmockする。

#[tokio::test]
async fn test_make_mock_get_v2_profile_raw_success() {
    // モック準備
    let mut server = Server::new_async().await;
    let mock = server
        .mock("GET", "/v2/profile")
        .match_header(
            "authorization",
            "Bearer test_access_token",
        )
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(json!({
            "userId": "U123456",
            "displayName": "Test User",
            "pictureUrl": "https://example.com/picture.jpg",
            "statusMessage": "I'm happy!"
        }).to_string())
        .create_async()
        .await;


    let res = get_v2_profile::execute(
        "test_access_token",
        &LineOptions {
            // APIの向き先をmockitoのサーバーに変更
            prefix_url: Some(server.url()),
            ..Default::default()
        },
    )
    .await
    .unwrap();

    assert_eq!(res.0.user_id, "U123456");
    assert_eq!(res.0.display_name, "Test User");
    assert_eq!(
        res.0.picture_url,
        Some("https://example.com/picture.jpg".into())
    );
    assert_eq!(res.0.status_message, Some("I'm happy!".into()));

    // モックが呼ばれたか確認
    mock.assert_async().await;
}

自由に値が設定できてMockは便利です。

普通にエラーのモックをする

#[tokio::test]
async fn test_make_mock_get_v2_profile_raw_failure() {
    // モック準備
    let mut server = Server::new_async().await;
    let mock = server
        .mock("GET", "/v2/profile")
        .match_header(
            "authorization",
            "Bearer test_access_token",
        )
        .with_status(400)
        .with_header("content-type", "application/json")
        .with_body(json!({
            "message": "error occurred"
        }).to_string())
        .create_async()
        .await;


    let res = get_v2_profile::execute(
        "test_access_token",
        &LineOptions {
            // APIの向き先をmockitoのサーバーに変更
            prefix_url: Some(server.url()),
            ..Default::default()
        },
    )
    .await;

    match res {
        Err(Error::Line(response, status_code, _header)) => {
            assert_eq!(status_code, 400);
            assert_eq!(response.message, "error occurred");
        }
        _ => panic!("Unexpected response"),
    }

    // モックが呼ばれたか確認
    mock.assert_async().await;
}

エラーの確認もできました。

Mockをbuilderで作成する

例えばユーザー名をちょっと変えて何度も呼び出したりすると毎回モックを書くことになります。ほとんど同じコードなので、コピペでは数が増えていやですね。

そこでbuliderを作って設定したいところだけ設定できるようにします。

#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default)]
#[builder(setter(into))]
#[builder(default)]
#[builder(field(public))]
pub struct MockParams {
    pub access_token: String,
    pub status_code: usize,
    pub user_id: String,
    pub display_name: String,
    pub picture_url: Option<String>,
    pub status_message: Option<String>,
    pub error_message: Option<String>,
}

pub async fn make_mock(server: &mut Server, builder: Option<MockParamsBuilder>) -> Mock {
    let mut builder = builder.unwrap_or_default();
    if builder.access_token.is_none() {
        builder.access_token("test_access_token".to_string());
    }
    if builder.status_code.is_none() {
        builder.status_code(200usize);
    }
    if builder.user_id.is_none() {
        builder.user_id("U123456".to_string());
    }
    if builder.display_name.is_none() {
        builder.display_name("Test User".to_string());
    }
    if builder.error_message.is_none() {
        builder.error_message("error occurred".to_string());
    }
    let params = builder.build().unwrap();

    let body_json = if params.status_code == 200 {
        let mut json = json!({
            "userId": params.user_id,
            "displayName": params.display_name,
        });
        if params.picture_url.is_some() {
            json["pictureUrl"] = params.picture_url.clone().into();
        }
        if params.status_message.is_some() {
            json["statusMessage"] = params.status_message.clone().into();
        }
        json
    } else {
        json!({
            "message": params.error_message
        })
    };

    server
        .mock("GET", "/v2/profile")
        .match_header(
            "authorization",
            format!("Bearer {}", params.access_token).as_str(),
        )
        .with_status(params.status_code)
        .with_header("content-type", "application/json")
        .with_body(body_json.to_string())
        .create_async()
        .await
}

builderを使った成功

#[tokio::test]
async fn test_make_mock_get_v2_profile_success() {
    let mut server = Server::new_async().await;
    let mut builder = MockParamsBuilder::default();
    builder.picture_url(Some("https://example.com/picture.jpg".into()));
    builder.status_message(Some("I'm happy!".into()));
    let mock = make_mock(&mut server, Some(builder)).await;

    let res = get_v2_profile::execute(
        "test_access_token",
        &LineOptions {
            prefix_url: Some(server.url()),
            ..Default::default()
        },
    )
    .await
    .unwrap();

    assert_eq!(res.0.user_id, "U123456");
    assert_eq!(res.0.display_name, "Test User");
    assert_eq!(
        res.0.picture_url,
        Some("https://example.com/picture.jpg".into())
    );
    assert_eq!(res.0.status_message, Some("I'm happy!".into()));

    mock.assert_async().await;
}

変更が必要なところだけ値を設定すればデフォルト値でいけます。

builderを使った失敗

async fn test_make_mock_get_v2_profile_failure() {
    let mut server = Server::new_async().await;
    let mut builder = MockParamsBuilder::default();
    builder.status_code(400usize);
    let mock = make_mock(&mut server, Some(builder)).await;

    let res = get_v2_profile::execute(
        "test_access_token",
        &LineOptions {
            prefix_url: Some(server.url()),
            ..Default::default()
        },
    )
    .await;

    match res {
        Err(Error::Line(response, status_code, _header)) => {
            assert_eq!(status_code, 400);
            assert_eq!(response.message, "error occurred");
        }
        _ => panic!("Unexpected response"),
    }

    mock.assert_async().await;
}

エラーはステータスコードの変更だけでいけます。

まとめ

Mockitoを使ったbuilderを作ることで、同じAPIのモックを何度も書く場合に楽になりました。

Discussion