JavaScript
jest

Jestの曖昧な比較

Jestは、expect(...).toEqual(...)などを使って、オブジェクトや配列の実際の値と想定値との比較を行う時に、expect.anythingやexpect.anyなどを使うことで曖昧に比較を行うことができます。便利なのでよく使っていますが、言及している記事が見当たらず、実はあまり知られていないのでは?と思ったので紹介します。

expect.anything

expect.anything()

Jestのテストケースで、オブジェクトや配列の中身の比較する場合、expect(...).toEqual(...)を使います。

expect(obj).toEqual({
  id: 1,
  name: 'hoge'
});

その時、比較するオブジェクトや配列にタイムスタンプなど動的なデータが含まれている場合、単純にtoEqualを使って比較ができません。toEqualの利用をやめて、オブジェクトのフィールドごと(もしくは、配列の要素ごと)にtoBeなどを利用して個別に検証するのはフィールド数・要素数が多ければ多いほど面倒です。

// テスト実行時に毎回変動する動的なデータ
obj.createdAt = Date.now();

expect(obj).toEqual({
  id: 1,
  name: 'hoge',
  createdAt: Date.now() // 一致しない
});

// プロパティ個別で比較するのはプロパティが増えると面倒
expect(obj.id).toBe(1);
expect(obj.name).toBe('hoge');
expect(obj.createdAt).toBeInstanceOf(Number);

上記の例のcreatedAtが「値が入っていればよい」のであれば、expect.anything()を使うとtoEqualを使って比較できます。expect.anything()toEqualの値として使うと、検証値がnull、undefined以外の値であれば比較をパスします。上記の例を、toEqualexpect.anything()を使って書くと以下のようになります。createdAt: expect.anything()となっているので、obj.createdAtがnull, undefined以外の値であれば、この比較はパスします。

expect(obj).toEqual({
  id: 1,
  name: 'hoge',
  createdAt: expect.anything() // null、undefined以外だったらOK
});

上記の例はオブジェクトのフィールドでexpect.anything()を使っていますが、配列の要素でも可能です。また、toHaveBeenCalledWithでも使えます。

// 配列の要素でも使える
expect([1, 'hoge', Date.now()]).toEqual([1, 'hoge', expect.anything()]);

// toHaveBeenCalledWithでも使える
const callback = jest.fn();
callback(1, 'hoge', Date.now());
expect(callback).toHaveBeenCalledWith(1, 'hoge', expect.anything());

expect.any

expect.any(constructor)

「null、undefined以外の値」ではゆるすぎる!という場合は、expect.any(constructor)を使うと、constructorのインスタンスであればパスするようにできます。

expect(obj).toEqual({
  id: 1,
  name: 'hoge',
  // Numberインスタンスであればパスする
  createdAt: expect.any(Number)
});

expect.stringMatching, expect.stringContaining

expect.stringContaining(string)
expect.stringMatching(regexp)

expect.stringMatchingは正規表現でマッチすれば、expect.stringContainingは引数の文字列が含まれれば、比較をパスします。どちらもtoEqualtoHaveBeenCalledWithの引数内で利用できます。

const obj = {
  id: 'prefix-1',
  name: '山田太郎'
}
expect(obj).toEqual({
  // obj.idが/^prefix-/にマッチすれば正
  id: expect.stringMatching(/^prefix-/),
  // obj.nameに"太郎"が含まれば正
  name: expect.stringContaining('太郎')
});

まとめ

知ってる人は知っている、知らない人は覚えてね。

追記:expect.arrayContaining, expect.objectContaining

expect.arrayContaining, expect.objectContainingについて、コメントで指摘されたので追記します。

expect.arrayContaining

expect.arrayContaining(array)

expect.arrayContainingは、渡した配列の要素が比較対象に全て含まれているかを検証します。toEqualtoHaveBeenCalledWithなどと組み合わせて利用します。また、expect.arrayContaing内でexpect.anyexpect.anythingを利用できるので、「配列に〜型が含まれている(もしくは、含まれていない)」ことを検証できます。下記サンプルの3つ目のexpectでは、arrにNumber型が含まれていないことを検証しています。

  const arr = ['a', 'b', 'c', 'd'];
  // arrに'a'と'c'が含まれているか
  expect(arr).toEqual(expect.arrayContaining(['a', 'c']));

  const obj = { arr }
  // obj.arrに'a'と'c'が含まれているか
  expect(obj).toEqual({
    arr: expect.arrayContaining(['a', 'c'])
  });


  // arrにNumberが含まれないことを検証
  // expect.arrayContaining内で
  // expect.any, expect.anythingを利用できる
  expect(arr).not.toEqual(
    expect.arrayContaining([expect.any(Number)])
  );

expect.objectContaining

expect.objectContaining(object)

expect.objectContainingは、渡したオブジェクトのキー・バリューの組み合わせが比較対象に含まれているか検証します。toEqualtoHaveBeenCalledWithなどと組み合わせて利用します。expect.arrayContainingと同様、expect.objectContaining内でexpect.anyexpect.anythingが利用できるので、「aフィールドにStringが代入されているオブジェクト」みたいな比較が可能です。

  const obj = {
    a: 'a',
    b: 'b',
    c: 'c',
  };

  // objにa: 'a'、c: 'c'が含まれているか
  expect(obj).toEqual(
    expect.objectContaining({ a: 'a', c: 'c' })
  );

  // objectContainingの中でexpect.anyなどを使いことも可能
  expect(obj).toEqual(
    expect.objectContaining({
      a: 'a',
      c: expect.any(String)
    })
  );

Jestのドキュメントには(全てが翻訳されているわけではありませんが)日本語版があります。JestはJasmineベースなのでドキュメントを読まなくてもなんとなく使えてしまいますが、暇な時に眺めてみると何か新しい発見があるかもです。