【Jest】mockの初期化ができない時の備忘録

# はじめに
TypeScriptで開発を行う際、テストツールとしてJestを利用しました。
その際、特定のテストに対しモックを設定したのですが、他のテスト結果にも影響してしまったので、Mockの初期化について整理し、備忘録としてまとめてみました。
Math.randomとDate.getMonthのモック化を例に紹介します。

# 目次
1. [モック化と初期化](#Jestでのモック化と初期化方法)
1. [サンプルコード](#サンプルコード)
1. [ハマったところ](#ハマったところ)
    1. タイポ
    1. 初期化位置
    1. afterEach()での初期化

# 環境
node: 16.4.2
ts-node: 10.4.0
typescript: 4.5.5
@types/jest: 27.4.0
jest: 26.6.3
ts-jest: 26.5.6

# Jestでのモック化と初期化方法

## jest.spyOn()でのモック化
random関数が必ず0を返すモック

```
const spyRandom = jest.spyOn(global.Math, 'random').mockReturnValue(0);
```

上記以外の方法でもモック化は可能です。
その他、モック化の詳しい説明はこちらの記事にまとめています。

https://qiita.com/shirotaro-jp/items/cba7d57584b201cff1c0

## 元の（モックされていない）実装に戻す
### mockFn.mockRestore
```
spyRandom.mockRestore();
```

または、

```
(global.Math.random as jest.Mock).mockRestore();
```
>mockFn.mockRestoreはjest.spyOnによって作成されたモックに対してのみ動作することに注意して下さい。
このため手動で jest.fn()を割り当てた場合は自分で復元作業を行わなければならないことに気をつけて下さい。
（JEST API Referenceより）

または、
### restoreAllMocks

```
jest.restoreAllMocks();
```
>全てのモックを初期値に戻します。
すべてのモック関数で.mockRestore（）を呼び出すのと同じです。
（JEST API Referenceより）

または、
### resetMocks
jestオプションのresetMocksをtrueにする
>すべてのテストの前に、モックの状態と実装を自動的に復元します。
jest.restoreAllMocks()各テストの前に呼び出すのと同じです。
Jest の構成はプロジェクトの package.json または jest.config.js か jest.config.ts ファイル、または --config オプションを通して設定できます。
（JEST API Referenceより）

# サンプルコード
```sampleMethod.ts
export default class sampleMethod {
    public getRandomNumber(): number {
        return Math.random()
    }
    public getMonth(): number {
        return new Date().getMonth()
    }
    public getHoge(): string{
        return 'Hoge'
    }
}
```

↓各テスト内で初期化する場合

```sampleMethod.test.ts
import sampleMethod from './sampleMethod';

describe('テストを実行するたびに値が異なる関数', () => {
    const SampleMethod = new sampleMethod;
    test(`Math.random`, function () {
        // Math.randomが必ず0を返すモック
        const spyRandom = jest.spyOn(global.Math, 'random').mockReturnValue(0);

        const resultNumber = SampleMethod.getRandomNumber();

        // 初期化
        spyRandom.mockRestore();

        expect(resultNumber).toBe(0);
    });
    test(`Date.getMonth`, function () {
        // 任意の日時を指定したモック
        const mockDate = new Date('2022-01-01T01:00:00');
        const spyDate = jest.spyOn(global, 'Date').mockImplementation(() => mockDate as unknown as string);

        const resultMonth = SampleMethod.getMonth();

        // 初期化
        spyDate.mockRestore();

        expect(resultMonth).toBe(0);
    });
    test(`Hoge`, function () {
        const resultStr = SampleMethod.getHoge();

        expect(resultStr).toBe('Hoge');
    });
})
```
# ハマったところ

## タイポ
使い慣れてない言語とか関数とか使うと、やっぱりタイポを見落としがちです。
あとで、「ここかよ〜」ってならないために、まづはタイポのチェックを。

## 初期化位置
`mockFn.mockRestore()`を書く位置によっては、そのテスト内でエラーがあった時に初期化のコードが実行されず、それ以降のテストに生き残ったモックが影響を与えてしまいます。
`expect`の前に`mockFn.mockRestore()`を書いた方が良さそうです。

```TypeScript
const SampleMethod = new sampleMethod;
test(`Math.random`, function () {
    // Math.randomが必ず0を返すモック
    const spyRandom = jest.spyOn(global.Math, 'random').mockReturnValue(0);

    const resultNumber = SampleMethod.getRandomNumber();
    expect(resultNumber).toBe(0);　　// ←ここでエラーが出ると、この下に書いた初期化コードが実行されない

    // 初期化 ↓↓ここに書いちゃった
    spyRandom.mockRestore();
});
```

## afterEach()での初期化
各テスト内で`mockFn.mockRestore()`するのがめんどくさいので、`afterEach()`で初期化をしようとしたところ、またハマりました。

```TypeScript
afterEach(() => {
    (global.Math.random as jest.Mock).mockRestore();
    (global.Date as unknown as jest.Mock).mockRestore();
});
```

これで初期化しようとすると、モックを利用しないテストで怒られる

```Console
● テストを実行するたびに値が異なる関数 › Math.random
  TypeError: global.Date.mockRestore is not a function
● テストを実行するたびに値が異なる関数 › Date.getMonth
  TypeError: global.Math.random.mockRestore is not a function
● テストを実行するたびに値が異なる関数 › Hoge
  TypeError: global.Math.random.mockRestore is not a function
```

ちなみに、`jest.restoreAllMocks()`は問題ないです。
ただ、意図しないモックまで消えてしまわないかは不安です。

```TypeScript
afterEach(() => {
    jest.restoreAllMocks()
});
```

- 代替案

モック化した値を返してほしいのが、１回だけなら、`.mockReturnValueOnce()`を使ってみるのも初期化忘れの対策になるかもしれません。

```TypeScript
// Math.randomが1度だけ、必ず0を返すモック
const spyRandom = jest.spyOn(global.Math, 'random').mockReturnValueOnce(0);
```

# 最後に
モックの初期化忘れの対策として、より良い方法があれば、ぜひ教えてください。