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をやってきた人間からすると、結構近い感覚で書けるので気持ち楽。（ユニテ自体あまり変わらないのかもしれないが