🛠️

Jestのexpect(matcher)を完全に理解する

2021/12/09に公開

本記事は estie Advent Calendar 2021 9日目の記事です。

概要

  • なんとなくJestを使ってしまっている人が
  • 基本的なexpectメソッドを「完全に理解」し
  • よいテストコードを書けるようになる

ことを目的とし、公式ドキュメントを基にした整理・解説をおこないます。
記事の構成については目次をご覧下さい。それではいってみよう!

等価判定メソッドの基礎

まず最初に、Jestの最も基本的な等価判定メソッドである toBe, toEqual, toStrictEqualの仕様を理解し、適切に使い分けましょう。
他の高機能なマッチャの仕様のベースになっているので、これさえわかれば当分困りません。

結論から言うと、以下のような使い分けが妥当と考えています。

- toBe
  - プリミティブ値の比較
  - オブジェクトの参照先の比較
- toEqual
  - オブジェクトの値の比較
- toStrictEqual
  - 継承元クラスやundifinedなプロパティを含む厳密な値の比較

それでは各マッチャの仕様を掘り下げてみましょう。

toBe(value)

Jestドキュメント

Use .toBe to compare primitive values or to check referential identity of object instances.
It calls Object.is to compare values, which is even better for testing than === strict equality operator.

  • object instances とは、Object, Array, Mapなど typeof instance === "object" となる( null 以外の)全てのオブジェクトと解釈できます。

  • Object.is は、大まかには、 === (strict equal) から、NaNと0に関する(人間にとって直感的ではない挙動を)除いたものと理解できます。MDNドキュメント

JavaScriptのテスティングは(暗黙的な型変換のない) === が基本となりますが、 Object.is との違いを確認してみましょう。

NaN === NaN // false
+0 === -0 // true

Object.is(NaN, NaN) // true
Object.is(+0, -0) // false

ソフトウェアのテストにおいては Object.is のほうが誤解が少なく、使い勝手がよさそうですね。

  • check referential identity of object instances 、つまり「同一のインスタンスの参照であるかどうかの確認」ということになります。これは前述の通り Object.is によって担保されています。

Object.is のMDNドキュメントを見てみます。

Two values are the same if one of the following holds:
(中略)

  • both the same object (meaning both values reference the same object in memory)

とあるので、比較対象の2つのオブジェクトが「参照渡しと値渡し」で言うところの同じ参照であるかどうか、という判定となります。

実際に動かしてみます。

const hoge = { key: 'hoge' };
const anotherHoge = { key: 'hoge' };

// 同じ参照
expect(hoge).toBe(hoge);
// 同じ値だが違う参照
expect(hoge).not.toBe(anotherHoge);
expect(hoge).not.toBe({ key: 'hoge' });
expect(hoge).not.toBe({ ...hoge });

最初に宣言した hoge という変数への参照を渡す場合のみ、 toBe が成功することがわかります。

toEqual(value)

Jestドキュメント

Use .toEqual to compare recursively all properties of object instances (also known as "deep" equality).

オブジェクトの中身が再帰的に同じか否か、を"deep equality"と呼びますが、それを比較するのが toEqual となります。
先ほどと同じ hoge オブジェクトで確認してみましょう。

const hoge = { key: 'hoge' };
const anotherHoge = { key: 'hoge' };

// 同じ参照
expect(hoge).toEqual(hoge);
// 同じ値だが違う参照
expect(hoge).toEqual(anotherHoge);
expect(hoge).toEqual({ key: 'hoge' });
expect(hoge).toEqual({ ...hoge });

このように、「違う参照先でも値が同じ」であれば toEqual は成功します。

ではプリミティブ値については toBetoEqual どちらを使えばよいかというと、どちらかと言えばtoBeのほうが言語的意味合いは近くなる(プリミティブ値は参照渡しの対象にならない)ので、そちらを推奨とする意見が多いようです。

参考: eslint-plugin-jest/docs/rules/prefer-to-be.md

toStrictEqual(value)

Jestドキュメント

Use .toStrictEqual to test that objects have the same types as well as structure.

こちらはVue.jsなどのフレームワーク上ではあまり使用機会はなさそうですが、 toEqual よりも厳格な値の比較となります。 toEqual に対して以下2点の違いがあります。

  • undifinedが指定されているプロパティに関して、その同値性を比較する
  • インスタンスの生成元クラスが同じであるかを比較する

実際に動かしてみます。

// undifinedが指定されているプロパティ
expect({ id: undefined, key: 'hoge' }).toEqual({ key: 'hoge' });
expect({ id: undefined, key: 'hoge' }).not.toStrictEqual({ key: 'hoge' });

// インスタンスの生成元クラス
class CustomClass {
  constructor(hoge) {
    this.key = hoge;
  }
}
const hogeFromCustomClass = new CustomClass('hoge');
const hoge = { key: 'hoge' }; // 値は hogeFromCustomClass と同じ
expect(hogeFromCustomClass).toEqual(hoge);
expect(hogeFromCustomClass).not.toStrictEqual(hoge);

モダンな開発環境でJavaScriptのClassはあまり利用しないイメージはありますが、使いどころが全くないわけではなさそうです。

配列のテスト

参照が一致していることを確認する

前述のように toBe(value) で確認します。

値が一致していることを確認する

前述のように toEqual(value) もしくは toStrictEqual(value) で確認します。

配列の長さのテスト

Array.prototype.length よりも .toHaveLength(number) を使うと読みやすくなります。

const actual = [1, 2, 3]
// before
expect(actual.length).toBe(3);
// after
expect(actual).toHaveLength(3);

.length を持っていれば使えるので、stringに対しても使うことができます。
※ 一般的に array-like object と呼ばれているものも .length プロパティを持つため、使うことができます。参考: array-like object っていったい何?iterable との違いは?言語仕様に立ち返って説明する - Qiita

要素が配列に含まれていることのテスト

「含まれている」が「参照が同じ」と「値が同じ」のどちらの意味であるかで、使うマッチャが変わってきます。

toBe(value) 基準で参照をテストする

参照は Array.prototype.includes() でも確認できますが、 toContain(item) を使うことができます。

  • 要素がプリミティブ値の場合
const primitiveValueArray = [1, 2, 3];
// before
expect(primitiveValueArray.includes(1)).toBeTruthy();
// after
expect(primitiveValueArray).toContain(1);
  • 要素がオブジェクトの場合
const hoge = { key: 'hoge' };
const hogeFoo = [hoge, { key: 'foo' }];
// before
expect(hogeFoo.includes(hoge)).toBeTruthy();
// after
expect(hogeFoo).toContain(hoge);

toEqual(value) 基準で値をテストする

参照渡しになっていない要素については toContain(item) を使うとテストが失敗するので、toContainEqual(item) を使います。

const hogeFoo = [{ key: 'hoge' }, { key: 'foo' }];
expect(hogeFoo).toContainEqual({ key: 'hoge' });
// toContain(item) は失敗する
expect(hogeFoo).not.toContain({ key: 'hoge' });

オブジェクト(連想配列)のテスト

参照が一致していることのテスト

前述のように toBe(value) で確認します。

値が一致していることのテスト

前述のように toEqual(value) もしくは toStrictEqual(value) で確認します。

特定のプロパティがオブジェクトに含まれていることのテスト

toHaveProperty(keyPath, value?) を使えます。

const hoge = { key: 'hoge' };
// before
expect(hoge.key).toEqual('hoge');
// after
expect(hoge).toHaveProperty('key', 'hoge');

toEqual(value) に比べて、テストが失敗したときのメッセージが親切になるのでおすすめです。

toHaveProperty(keyPath, value?) の比較は toEqual(value) の基準となり、参照が一致しているかの確認はできません。

const hoge = { hoge: { key: 'hoge' } };
// value? に参照を渡さなくてもテストは成功する
expect(hoge).toHaveProperty('hoge', { key: 'hoge' });

特定のkey-valueペアがオブジェクトに含まれている(部分集合になる)ことのテスト

toMatchObject(object) を使えます。

const hoge = { id: 1, name: 'hoge', address: 'foo' };
// before
Object.entries({ name: 'hoge', address: 'foo' }).forEach(([key, value]) => {
  expect(hoge[key]).toEqual(value);
});
// after
expect(hoge).toMatchObject({ name: 'hoge', address: 'foo' });

toMatchObject を使わない場合に比べてだいぶ短くなります。

公式ドキュメントにもあるようにtoMatchObjectに更にexpectマッチャを渡すこともできます。ファクトリメソッドのテストなどで活躍しそうですね。

const hoge = { id: 1, type: 'hoge', name: 'foo' };
expect(hoge).toMatchObject({
  id: expect.any(Number),
  type: 'hoge',
});

モックしたメソッドのテスト

モックしたメソッドはテストケースの中で「呼ばれたか」だけではなく、

  • 何回呼ばれたか
  • 何回目に何が渡されたか

を全て確認したほうがベターです。

特に、弊社でも利用しているVuex Storeのテストにおいては、モック対象となるcommitやgettersは同じモックが数回にわたって・違う引数で呼ばれるパターンがあるので、注意して実装をおこなっています。

const targetMethod = (func) => func('some argument');

const mockDependency = jest.fn();
targetMethod(mockDependency);
// before
expect(mockDependency).toHaveBeenCalled(1);

上記は .toHaveBeenCalled() を用いて、モックしたメソッドが「呼ばれたか」だけを確認するパターンです。トータルで呼ばれた回数やその引数については確認できていません。

// better
expect(mockDependency.mock.calls.length).toBe(1);
expect(mockDependency.mock.calls[0]).toEqual(['some argument']);

mock.calls を用いて、呼ばれた回数と引数を確認できました。機能的には十分ですが、もう少し読みやすくしてみましょう。

// more better
expect(mockDependency).toBeCalledTimes(1);
expect(mockDependency).nthCalledWith(1, 'some argument')

だいぶ自然言語に近く、読みやすくなりました。
注意点としては、実際に渡された引数が1個だけの場合、以下のような違いが生まれます。

  • nthCalledWith では実際の引数と同じ型('some argument')でexpectできる
  • mock.calls[index] には必ず配列(['some argument'])が入っている

2個以上の引数を渡す場合は両者とも配列でexpectをすることになります。

最後に

いかがでしたでしょうか。Jestにはまだまだここに書き切れなかった多彩なexpectメソッドがあり、使いこなせば使いこなすほど気持ちよくなれます。

ちょっとテスト面倒だな…と思っても、思わぬところで役に立つマッチャがあったりします。
定期的に公式ドキュメントを見直して、気持ちよくテストを書いていきましょう!

ここまで読んで下さってありがとうございました!

Discussion