VitestやJestのテストコードでtoEqualは不適切
VitestやJestのテストコードでオブジェクトや配列の検証を書く際、なんとなくtoEqualを使っているコードを良く見かけます。
toEqualには厳格に比較されない「3つのゆるさ」があり、本来ならテストが落ちるべきバグをスルーしてしまうリスクがあります。
より堅牢なテストコードを書くためには、基本的にtoStrictEqualを選択するのが安全です。
toEqualの「ゆるさ」とは以下の3つです。
1. undefinedプロパティの存在を無視する
toEqualはプロパティの値がundefinedである場合と、そのキー自体が存在しない場合を区別しません。
const actual = { id: 1, name: 'Alice' }
const expected = { id: 1, name: 'Alice', role: undefined }
// PASSしてしまう(role キーが存在しないのに成功する)
expect(actual).toEqual(expected)
// FAILになる(キーの有無まで厳密にチェック)
expect(actual).toStrictEqual(expected)「APIのレスポンスに不要なキーが含まれていないか」などを正しく検証したい場合、toEqual ではバグを見逃す可能性があります。
2. クラスのインスタンスとプレーンオブジェクトを同一視する
toEqualは型をチェックしないため、「クラスから生成したインスタンス」と「通常のオブジェクトリテラル」を同じものとして扱います。
class User {
constructor(public name: string) {}
}
const userInstance = new User('Alice')
const plainObject = { name: 'Alice' }
// PASSしてしまう
expect(userInstance).toEqual(plainObject)
// FAILになる(UserクラスとObjectの違いを検知)
expect(userInstance).toStrictEqual(plainObject)ドメインモデルのクラスを返しているはずが、誤ってプレーンなオブジェクトを返してしまっているようなケースをtoEqualでは検知できません。
3. 配列の空要素とundefinedを区別しない
JavaScriptの配列で発生する「empty」と「undefined」の比較でも差が出ます。
const sparseArray = [, 'apple'] // [ <1 empty item>, 'apple' ]
const undefinedArray = [undefined, 'apple']
// PASSしてしまう
expect(sparseArray).toEqual(undefinedArray)
// FAILになる
expect(sparseArray).toStrictEqual(undefinedArray)このコードはどちらも [0] で取得するとundefinedなので同じだと誤解している方をたまに見かけますが、mapなどの処理で違いが発生するので別物です。
const sparseArray = [, 'apple']
const undefinedArray = [undefined, 'apple']
sparseArray.map((item) => item ?? 'default')
// [ <1 empty item>, 'apple' ](1要素目は変換されない)
// undefinedの場合は正しく処理される
undefinedArray.map((item) => item ?? 'default')
// ['default', 'apple']まとめ:原則としてtoStrictEqualを使うべき
toEqualが提供する曖昧さは、多くの場合において意図しないテスト結果を引き起こす原因になります。
そのため、toEqaulの使用は特に理由がない場合は避けたほうが良いです。
VitestやJestを使用している場合はESLintにprefer-strict-equalのルールを追加して、toEqualの使用を禁止する措置もおすすめします。
私の経験上、多くの現場(企業)のテストコードでtoStrictEqualではなくtoEqualが使われているため、心当たりのある方はぜひプロジェクトのコードを確認してみてください。


