💫

Jestを導入したくてJest入門しました【マッチャー関数編】

2023/12/18に公開

はじめに

日頃業務を行う中で、改修や機能追加をする際にテストが書いていないと、対応後に手動でデバックして確認する作業をする必要があり、生産性があまりよろしくないと感じたため、Jestの導入を検討しましたが、自分はバックエンドのRspec以外のテストフレームワークを使用した経験がなく、Jestの導入を提案する土俵にも立てていなかったため、まずはJest入門する必要があり勉強をはじめました。

その時にまとめた内容の一部を今日は共有したいと思います。
全て共有するととても膨大なため、まずは「マッチャー関数」についての内容からにしたいと思います。

JestでTypescirptのコードを書く準備

まずはじめにセットアップをします。
セットアップ方法は「サバイバルTypescirpt」に丁寧に書かれているため、こちらを参考に行いました。
https://typescriptbook.jp/tutorials/jest

Jestのインストール

yarn add -D 'jest@^28.0.0' 'ts-jest@^28.0.0' '@types/jest@^28.0.0'

JestはそのままではTypescirptのテストをおこなえないため、以下のコマンドで jest.config.ts ファイルを作成する必要があるので覚えておきましょう。

yarn ts-jest config:init

Jestでテストコードを実行するための決まり事

Jestのテストファイル名は、 .test.ts.spec.ts で終わるようにしてください。
こうすることで、JestがJestのテストファイルとして認識してテストを行うことができます。

テストコードの基本構造

  • Jestでは、 test関数 を使用してテストを行う
    it関数 でもtest関数と同じようにテストできるので、テストするプロジェクトに合わせてどちらにするか決めてもらって大丈夫です。
    もし既存プロジェクトのバックエンド側のテストフレームワークで、Rspecが使われていて、そちらでitが使われているならそちらに合わせても良いかもしれないです。

  • expect関数で期待するテストを行う
    expect関数の引数にテスト結果を入れて、そちらを toBe 関数で評価します。
test('テストケース文言を書く', () => {
  const result = true // テスト結果
  const expected = true // 期待する値
  expect(result).toBe(expected) // テストの評価を行う
})

基本的なマッチャー関数

  • toBe関数
    • プリミティブ値や同じオブジェクト参照の比較に使用します。
    • === に相当する厳密な比較を行います。
  • toEqual関数
    • オブジェクトや配列の構造を再帰的に比較します。
    • プロパティの値が同じ場合に等しいと判定しますが、生成元クラスは考慮しません。
  • toStrictEqual関数
    • toEqual関数に似ていますが、より厳密に判定したいケースで使用します。
    • undefined のプロパティやオブジェクトの生成元クラスも評価に含めます。

toBe関数

文字列や数値の比較は同じであれば評価はtrueになります。

expect(1).toBe(1)    // テストは成功する
expect("1").toBe(1)  // テストは失敗する
  • 同じプロパティを持つオブジェクトの比較
    以下のように別々の関数で定義したオブジェクト同士だとfalseになります。
type CanType = {
  flavor: string
  ounces: number
}

const can1: CanType = {
  flavor: 'grapefruit',
  ounces: 12
}

const can2: CanType = {
  flavor: 'grapefruit',
  ounces: 12,
}

test('can1 と can2は等しくない', () => {
  expect(can1).toBe(can2)
})
⇩
結果はfalseになる

falseになる理由は、toBe関数では内部的に Object.is() 関数で評価をしているため、純粋な === での等しいで判断しているわけではないからです。
定義しているオブジェクトが違うとfalseになるので、それを前提に適切なテストケースで使用する必要があります。

toEqual関数

上記のようにオブジェクト同士をしっかり判定したい場合は、 toEqual()関数 を使用する必要があります。
toEqual関数は、 再帰的 に評価してくれるため正確にプロパティの値同士を判定してくれます。
ちなみに再帰的に評価するとはどういうことかというと、オブジェクトなど深いプロパティ同士も、しっかりと調べて評価してくれるということです。

例えば以下のオブジェクトがあった場合、

const hoge = {
  foo: 1,
  bazz: {
    num: 2,
    fooz: 1
  },
}

const zen = {
  foo: 1,
  bazz: {
    num: 2,
    fooz: 1
  },
}

expect(hoge).toEqual(zen) // true

このように深いプロパティであったとしても、外側から内部まで隅々までプロパティ同士を再帰的に比較してチェックして評価してくれます。
配列であってもオブジェクトであっても問題ありません。

toStrictEqual関数

toStrictEqual 関数は、生成元のクラスが同じかどうか評価したい場合に使用する関数です。
この関数を使用することで、同じプロパティを持つオブジェクト同士であっても、生成元のクラスが違うとエラーになります。

const can2: CanType = {
  flavor: 'grapefruit',
  ounces: 12,
}

class Can {
  flavor: string
  ounces: number

  constructor({ flavor, ounces }: CanType) {
  this.flavor = flavor
  this.ounces = ounces
  }
}

const can4 = new Can({
  flavor: 'grapefruit',
  ounces: 12
})test('can2 と can4は等しくないと評価される', () => {
 expect(can2).toStrictEqual(can4) // falseになる。
})

似ているが違う、toEqual関数とtoStrictEqual関数の使い分け

toEqual関数は、評価するプロパティに undefined があると評価しません。
対して、toStricEqual関数は、評価するプロパティに undefined があっても評価します。
なので、厳密に評価したい場合は、toStricEqual関数を使用することが望ましいです。
例として以下のように挙動が違います。

test('undefinedあるなしの評価比較', () => {
  // undefinedはスキップするのでtrueになる'
  expect({ foo: 'hoge', buzz: undefined }).toEqual({ foo: 'hoge' })

  // 'undefinedも厳密に評価するためfalseになる'
  expect({ foo: 'hoge', buzz: undefined }).toStrictEqual({ foo: 'hoge' })

  // trueになる
  expect({ foo: 'hoge', buzz: undefined }).toStrictEqual({ foo: 'hoge', buzz: undefined })

  // trueになる
  expect([, undefined, 1]).toEqual([undefined, , 1])

  // falseになる
  expect([, undefined, 1]).toStrictEqual([undefined, , 1])
})

配列の中身の順番が違うと以下のようにエラーになります。

expect([, undefined, 1]).toStrictEqual([undefined, , 1])Expected: [undefined, , 1]
Received: [, undefined, 1]

オブジェクトの中身や、配列の順番も正確に評価したいならtoStricEqual関数を使用することが望ましいです。
また、NullやUndefinedの判定時は toBe関数 よりも適したマッチャー関数があるので、次はそちらの紹介をします。

NullやUndefinedの評価をするマッチャー関数

toBeNull関数

Nullを評価したい場合は、 toBeNull 関数というマッチャーが使えます。

  test('nullの判定テスト', () => {
  expect(null).toBeNull() // true
})
  • toBe関数でもnullの評価は可能です。
  test('nullの判定テスト', () => {
  expect(null).toBe(null) // true
})

toBeUndefined関数

undefinedを評価したい場合は、 toBeUndefined 関数というマッチャー利用できます。

  test('undefinedのテスト', () => {
  expect(undefined).toBeUndefined() // true
})

こちらもNull同様、toBe関数でも undefinedの評価は可能です。

test('undefinedのテスト', () => {
  expect(undefined).toBe(undefined) // true
})

toBe関数と「toBeNull関数・toBeUndefined」の使い分け

  • toBeNull関数とtoBeUndefined関数を使用した方が良いケース

    • テストの意図がわかりやすい。
      Nullやundefinedを評価したい場合は、基本的にこちらの2つの関数を使用する方が、何をテストしているのかわかりやすくて読みやすいです。
      ちなみにtoBeNull関数も、toBeUndefined関数も厳密に評価できます。
  • toBe関数を使用した方が良いケース

    • 特にありません。
      toBe関数でも同じように評価できますが、ぱっと見で何をテストしているか視覚的にわかりにくいです。

小数点(浮遊小数点)の誤差を許容した数値の評価をするマッチャー関数

toBeCloseTo関数

浮動小数点を許容して数値を評価したい時に使用します。

例として、小数点の結果を判定する場合、0.1 + 0.2 = 0.3000004のようになるケースがあります。
これは浮遊小数点と言って、厳密には0.1とかではないからです。
なので、このようなケースで以下のように評価するとエラーになります。

test('0.1 + 0.2 は0.3になる', () => {
  expect(0.1 + 0.2).toBe(0.3) // false
})

このような浮遊小数点を許容する場合にtoBeCloseTo関数を使用します。

test('0.1 + 0.2 は0.3になる', () => {
  expect(0.1 + 0.2).toBeCloseTo(0.3)
})

小数点いくつまでを評価対象にするかの指定方法

下記のように、toBeCloseTo(0.3, 3)と第二引数で3と指定することで、小数点第3位までを評価対象にすることができます。

test('小数点氏も3桁目までを評価する', () => {
  expect(0.3 + 0.001).toBeCloseTo(0.3, 2)
})

〜より大きい・小さい判定をするマッチャー関数

  • toBeGreaterThan
    〜よりも大きいを判定する関数
  • toBeGreaterThanOrEqual、
    〜よりも大きいor等しいかを判定する関数(0 >= 3)
  • toBeLessThan
    〜よりも小さいを判定する関数
  • toBeLessThanOrEqual
    〜よりも小さいor等しいかを判定する関数(0 <= 3)

以下は結果が同じなため、toBeLessThanOrEqual関数を使った方が読み手が何を評価しているものかわかりやすいです。

expect(0.1 + 0.2).toBeLessThanOrEqual(0.4)
expect(0.1 + 0.2 <= 0.4).toBe(true)

文字列の部分一致を比較するマッチャー関数

単純な文字列の部分一致を評価したい場合

stringContaining 関数を使います。

const log1 = '10.0.0.3 - - [30/Jan/2023:12:20:12 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.74.0" "-"'

test('contain IP address Between 10.0.0.0 and 10.0.0.99', () => {
  const expected = /^10.0.0.([1-9]?[0-9]) /

  expect(log1).toEqual(expect.stringMatching(expected
})

直感的に部分一致、完全一致などを評価したい場合

toMatch 関数を使います。

const log1 = '10.0.0.3 - - [30/Jan/2023:12:20:12 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.74.0" "-"'

test('contain IP address Between 10.0.0.0 and 10.0.0.99', () => {
  const expected = /^10.0.0.([1-9]?[0-9]) /

  expect(log1).toMatch(expected)
})

stringContaining関数、toMatch関数どちらが良いか?

  • toMatch関数の方が望ましいです。
    こちらの方が直感的に何を評価してるのかわかりやすく、尚且つシンプルに書くことができるためです。

toBe関数で正規表現を活用して部分一致も可能

以下のようにtoBe関数と正規表現を組み合わせて、文字列の部分一致を行うこともできます。

const log1 = '10.0.0.3 - - [30/Jan/2023:12:20:12 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.74.0" "-"'

test('contain IP address Between 10.0.0.0 and 10.0.0.99', () => {
  const expected = /^10.0.0.([1-9]?[0-9]) /

  // 正規表現を生成して評価も可能
  const regex = new RegExp(expected)
  expect(regex.test(log1)).toBe(true)
})

配列や配列オブジェクトの部分一致の評価をするマッチャー関数

以下は期待する文字列が配列の中身にあるかを評価する関数です。

toContain関数

判定する対象がプリミティブ型の場合はこちらを使用します。

// 配列の部分一致
const fruitList = ['Apple', 'Lemon', 'Orange']

// 1つの要素が含まれていることを検証
test('appleがfruitListに含まれているか検証', () => {
  expect(fruitList).toContain('Apple') // true
})

toContainEqual関数

オブジェクトを判定したい場合に使用します。

const itemList = [
  { name: 'Apple', price: 100 },
  { name: 'Lemon', price: 150 },
  { name: 'Orange', price: 120 }
]

test('1つの要素が含まれていることを確認', () => {
  expect(itemList).toContainEqual({ name: "Apple", price: 100 })
})

arrayContaining関数

複数の中身を一括で判定したい場合に利用します。
使用する際は、 toEqual もセットで活用して再帰的に評価します。

const itemList = [
  { name: 'Apple', price: 100 },
  { name: 'Lemon', price: 150 },
  { name: 'Orange', price: 120 }
]

test('複数の要素が含まれていることを確認', () => {
  expect(itemList).toEqual(
  expect.arrayContaining([
    { name: 'Apple', price: 100 },
    { name: 'Orange', price: 120 }
  ])
  )
})
const array = ["test", "hoge", "hugo", "yahoo"]

test('複数のプリミティブ型も判定したい場合の確認', () => {
  expect(array).toEqual(
  expect.arrayContaining(['test', 'yahoo'])
  )
})

toContain関数、toContainEql関数、arrayContaining関数の使い分け

  • toContain関数
    配列の中の単体要素を判定したい場合に使用します

  • toContainEqual関数
    配列の中の1つのオブジェクトを判定したい場合に使用します

  • arrayContaining関数
    複数のオブジェクトや、配列の複数の要素を一括で判定したい場合に使用します。

まとめ

まずはJest入門のマッチャー編ということで、本記事は以上で終わりたいと思います。
マッチャー関数だけでとてもたくさん数がありますが、ユースケースによっては全て覚える必要はなく、それぞれの項目で代表的なものを適時使用するだけでも良さそうです。
それでは、次回は「コンポーネントテスト編」になります。

ゲームエイトテックブログ

Discussion