jestでのユニットテストを書いた際の個人的まとめ


jestでユニットテストを書いていたときのまとめ
スペルミスなどあるかもしれないけど雰囲気なので、気にしない

2020/04/01追記
resolves rejectsはawaitしないと、最後の呼び出し回数をカウントする際に失敗してしまう(何度呼んでも1回としてカウントされる)ので、修正

2020/04/03追記
clearAllMocksについて

そもそもユニットテストの記述に必要なもの

  1. 事前状態の作成(mockの作成
  2. 出力の評価(actualの比較、exceptionとの比較
  3. 事後状態の評価(mockしたオブジェクトの比較

テスト対象

意味はあまりないが、以下のようなコードがあるとして、service.tsをテストする

service.ts
import * as logic form './logic'

export testTarget = (a: number, b: number): Promise<number> => {
  const result = logic.calc(a, b)
  const saveResult = await logic.save({
    text: `save: ${result}`
  })
  if (!saveResult) {
    throw new Error('cannot add result')
  }
  return result
}
logic.ts
export calc = (a: number, b: number): number => {
  // 適当に計算をするものとする
  return 1
}

export save = (object: { text: string }): Promise<boolean> => {
  // 適当になにかに保存するものとする
  return Promise.resolve(true);
}

テストを書く

1. 事前状態の作成(mockの作成

私が基本的に使うのは以下3つ
* mockReturnValueOnce: 指定のオブジェクトを一度返す
* mockResolvedValueOnce: 指定のオブジェクトをPromise.resolveで包んで返す
* mockRejectedValueOnce: 指定のオブジェクトをPromise.rejectで包んで返す

mock対象の関数がPromiseを返すかで使い分ける。また、mock対象が複数回呼ばれるのであればチェインして書く。

2. 出力の評価(actualの比較、exceptionとの比較

私が基本的に使うのは以下3つ
* toBe: 単純比較(number、string、boolean
* toStrictEqual: Array、Objectの比較
* toThrowError: エラーの比較

3. 事後状態の評価(mockしたオブジェクトの比較

私が基本的に使うのは以下2つ
* toBeCalledTimes: 呼び出した回数の確認、回数が不定の場合は toHaveBeenCalled でカバー
* toHaveBeenCalledWith: 呼び出した際の引数の確認、複数回の場合は「順番に」複数回書く
* toHaveBeenNthCalledWith: 呼び出した際の引数の確認、n回目のものを検証したい場合に使う。nは1始まり

そして上記toHaveBeenCalledWithに与える引数として以下2つ
* expect.stringMatching:
* expect.not.stringMatching:

基本完全一致させる気持ちで書いているので、複雑な文字列で書くのが 面倒なとき 本質ではないときに使う程度

書いた感じ

service.spec.ts
import * as targets from './service'

jest.mock('./logic')
import * as logic from './logic'
const logicMock = logic as jest.Mocked<typeof logic>

describe('testTarget', () => {
  beforeEach(() => {
    // mockのクリア
    jest.clearAllMocks()
  })
  test('saveが成功する', async () => {
    // 1. 事前状態の作成
    logicMock.calc.mockReturnValueOnce(3)
    logicMock.save.mockResolvedValueOnce(true)

    // 2. 出力の評価
    const actual = targets.testTarget(1, 2)
    await expect(actual).resolves.toBe(2)

    // 3. 事後状態の評価
    expect(logicMock.calc).toBeCalledTimes(1)
    expect(logicMock.calc).toHaveBeenCalledWith(1, 2)
    expect(logicMock.save).toBeCalledTimes(1)
    expect(logicMock.save).toHaveBeenCalledWith({
      text: 'save: 3'
    })
  })
  test('saveが失敗する', async () => {
    // 1. 事前状態の作成
    logicMock.calc.mockReturnValueOnce(3)
    logicMock.save.mockRejectedValueOnce(new Error('なんかやばいエラー'))

    // 2. 出力の評価
    const actual = targets.testTarget(1, 2)
    await expect(actual).rejects.toThrowError(new Error('なんかやばいエラー'))

    // 3. 事後状態の評価
    expect(logicMock.calc).toBeCalledTimes(1)
    expect(logicMock.calc).toHaveBeenCalledWith(1, 2)
    expect(logicMock.save).toBeCalledTimes(1)
    expect(logicMock.save).toHaveBeenCalledWith({
      text: 'save: 3'
    })
  })
})

気をつけること

例えば、消し忘れていたなどでmockの挙動を定義していて、実際に呼ばれていない場合 clearAllMocks がmockを削除できず、そのまま次のテストでその挙動が実行されてしまう. jest: version 25.2.4

感想

他のユニテのフレームワークをきちんと理解しているわけではないが、かなりわかりやすく書ける。
Javaをやってきた人間からすると、結構近い感覚で書けるので気持ち楽。(ユニテ自体あまり変わらないのかもしれないが