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