Jestのexpect(matcher)を完全に理解する
本記事は estie Advent Calendar 2021 9日目の記事です。
概要
- なんとなくJestを使ってしまっている人が
- 基本的なexpectメソッドを「完全に理解」し
- よいテストコードを書けるようになる
ことを目的とし、公式ドキュメントを基にした整理・解説をおこないます。
記事の構成については目次をご覧下さい。それではいってみよう!
等価判定メソッドの基礎
まず最初に、Jestの最も基本的な等価判定メソッドである toBe
, toEqual
, toStrictEqual
の仕様を理解し、適切に使い分けましょう。
他の高機能なマッチャの仕様のベースになっているので、これさえわかれば当分困りません。
結論から言うと、以下のような使い分けが妥当と考えています。
- toBe
- プリミティブ値の比較
- オブジェクトの参照先の比較
- toEqual
- オブジェクトの値の比較
- toStrictEqual
- 継承元クラスやundifinedなプロパティを含む厳密な値の比較
それでは各マッチャの仕様を掘り下げてみましょう。
toBe(value)
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)
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
は成功します。
ではプリミティブ値については toBe
と toEqual
どちらを使えばよいかというと、どちらかと言えばtoBe
のほうが言語的意味合いは近くなる(プリミティブ値は参照渡しの対象にならない)ので、そちらを推奨とする意見が多いようです。
参考: eslint-plugin-jest/docs/rules/prefer-to-be.md
toStrictEqual(value)
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