🐣

JestのtoHaveBeenCalledWithで確認した引数が呼出後の変更に影響を受ける

2024/10/18に公開

はじめに

toHaveBeenCalledWithを使ってある関数が受け取った引数(オブジェクト)が想定通りか確認するテストを書いたとします。関数呼び出し後に引数として渡したオブジェクトの値を変更すると、toHaveBeenCalledWithの検証時に変更後の値で検証をしてしまい、関数は想定通りの引数を受け取ったにもかかわらずテストはNGとなってしまうというのが本記事ので取り扱う事象の概要です。
(筆者の忘備のために本記事を執筆しました)

挙動を再現したサンプルコード

例えば、以下のようなテストを書いた場合、mockFunctionに渡された時点の値ではなく、後から変更された{ name: "after", id: "test" }を参照します。そのため、テストが失敗してしまいます。

引数がオブジェクトの場合(NG)
test("objectテスト", () => {
  const mockFunction = jest.fn((object) => {
    object.id = "test";
  });
  const obj = { name: "before" };
  mockFunction(obj);

  obj.name = "after";

  const expectedParam = { name: "before" };
  expect(mockFunction).toHaveBeenCalledWith(expectedParam);
});

// Expected { "name": "before" }
// Received { "name": "after", "id": "test" }

以下に引数がprimitive値の場合について記載していますが、恐らくイメージ通りのため読み飛ばしていただいても大丈夫です。

サンプルコード
引数がprimitive値の場合(OK)
test("primitiveテスト", () => {
  const mockFunction = jest.fn((weight) => {
    return weight + 5;
  });
  const obj = { weight: 0 };
  mockFunction(obj.weight);

  obj.weight = 20;

  const expectedParam = 0;
  expect(mockFunction).toHaveBeenCalledWith(expectedParam);
});

// Expected 0
// Received 0

なぜこのことが起きるのか?

オブジェクトや配列は関数に渡される際、値のコピーではなく参照が渡されます。JestのtoHaveBeenCalledWithもこの参照を記録するため、関数が呼ばれた時点の引数(オブジェクト)の値が後から変更されると、その変更が反映されてしまいこのような挙動となるようです。

参考

調べて見ると、Githubでこの挙動に関するissueが発見できたので詳細はそちらをご確認いただければと思います。

Discussion