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

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

# 元の関数

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

```rust
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` で用意します。

```rust
/// 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(()));
```

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

```rust
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を呼び出す処理を行います。

```rust
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の指定処理が必要になります。

```rust
#[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` などに変換する必要があります。