0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustの関数のモック作成テクニック

Posted at

Rustのテストをする際に外部接続や稀にエラーが起こる場合の再現をしたいので、関数を好きな出力にができるようにMOCK化できる方法を紹介。

元の関数

今回はこのようなreqwestでHTTPのリクエストを行い、json形式の応答を得る、という関数を用意します。

pub async fn http_request(
    url: &str,
    body: &str,
) -> Result<serde_json::Value, Error> {
    let resp = Client::new()
        .post(url)
        .body(body)
        .send()
        .await?;
    let value: Value = resp.json().await?;
    Ok(value)
}

ちゃんとテストを構築しようとするのであれば、 actix_web などを使用して内部でサーバーを構築して、そこに対してリクエストを行うことになりますが、本質的にテストしたい部分が http_request の応答に応じた処理の場合、テストの作成カロリーが高くなります。

MOCK化

まずはMOCK処理を制御するためのstatic変数を once_cell::sync::Lazy で用意します。

/// http_request のMOCK関数
pub static MOCK_HTTP_REQUEST: once_cell::sync::Lazy<
    std::sync::RwLock<
        Option<
            Box<
                dyn Fn(&str, &str) -> Result<serde_json::Value, lambda_runtime::Error>
                    + Send
                    + Sync,
            >,
        >,
    >,
> = once_cell::sync::Lazy::new(|| std::sync::RwLock::new(None));

/// http_requestのモック設定用ロック
pub static MOCK_HTTP_REQUEST_LOCK: once_cell::sync::Lazy<std::sync::Mutex<()>> =
    once_cell::sync::Lazy::new(|| std::sync::Mutex::new(()));

次にもととなる関数を薄いラッパーで包む形に変更します。

pub async fn http_request(
    url: &str,
    body: &str,
) -> Result<serde_json::Value, Error> {
    http_request_inner(url, body)
}

async fn http_request_inner(
    url: &str,
    body: &str,
) -> Result<serde_json::Value, Error> {
    let resp = Client::new()
        .post(url)
        .body(body)
        .send()
        .await?;
    let value: Value = resp.json().await?;
    Ok(value)
}

この http_request にテスト実行時のみMOCKを呼び出す処理を行います。

pub async fn http_request(
    url: &str,
    body: &str,
) -> Result<serde_json::Value, Error> {
    #[cfg(test)]
    {
        let guard = MOCK_HTTP_REQUEST.read().unwrap();
        if let Some(f) = &*guard {
            return f(url, body);
        }
    }
    http_request_inner(url, body)
}

これでMock化は完成です。

テストコード

テストの実行時にはテストターゲットの実行前後にMOCKの指定処理が必要になります。

#[tokio::test]
async fn test() {
    // 重複してMockが呼ばれないようにロックが必須
    let lock = MOCK_HTTP_REQUEST_LOCK.lock().unwrap();

    // MOCKを指定
    *MOCK_HTTP_REQUEST.write().unwrap() = Some(Box::new(move |_url: &str, _body: &str| {
        Ok(json!({
            "response": "ok",
        }))
    }));

    // TODO: テストターゲットの処理

    // ロックを外す前に元の関数を呼び出すようにMOCKの指定を解除しておく
    *MOCK_SEND_JIRA_HTTP_REQUEST.write().unwrap() = None;
    
    // 検証処理の前にロックを外しておかないとassertでpanicした際に
    // MOCK_HTTP_REQUEST_LOCK.lock() がすべてエラーになってしまう
    drop(lock);

    // TODO: 検証処理
}

注意点

ジェネリクスを使用する場合

static変数を使用する場合、保持する関数にジェネリクスを持つことができません。
そのため、モック関数の引数は Box<impl Generics> を使用する必要があり、呼び出し時も引数をBox化するひと手間が必要になります。

MOCKのロックについて

LOCK中にpanicが発生した場合、以降はすべてポイゾニングエラーが発生します。
そのため、Mock関数の中で引数の値が正しいか、などのassertを直接行うと、他のテストに影響を与えるエラーが発生してしまいます。
これを避けるためにはMock内では直接assertは呼び出さず、Arc<Mutex>Arc<RwLock>Arc<AtomicBool> などを使用して値だけを格納しておき、最後の検証のタイミングでassertを呼び出すようにしましょう。

MOCKの応答について

Mockで作成する以上、応答がMockで作成できる必要があります。
そのため、例えば reqwest::Response など、クレート外部で作成する方法が無い関数を応答とすると、Mockが作成できないので、今回のサンプルのように自身で作成可能な serde_json::Value などに変換する必要があります。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?