[Jest+TypeScript] クラスと関数のモック化

# はじめに

ユニットテストを行う際に、依存しているサービスやAPIの応答によって実行結果が変わってしまうのはユニットテストとして不適切です。
そのため、依存している外部モジュールをモック化して理想的な挙動を実装することで、テスト対象のクラスや関数の動作だけを検証することができます。

今回は、JavaScriptのテストツールであるJestを利用して、TypeScriptで書かれたクラスや関数をモック化する方法を説明します。

## 環境
- Jest 25.1.0
- ts-jest 25.0.0
- Node.js 12.14.1
- TypeScript 3.7.5

## モック対象のクラスと関数

以下のクラスと関数をモック化します。

```typescript:walk.ts

export function walkFast(): string {
    return 'walk fast';
}

```

```typescript:Robot.ts

export class Robot {
    name = 'C-3PO';

    hello(): string {
        return `I am ${this.name}`;
    }
}
```



# 関数をモックにしたい

まずは、関数のモック化から説明します。テストコードは以下です。

```typescript:walk.ts

// importするモジュールを変数に割り当てる
import * as walkModule from './walk';

describe('walkFast関数のモック化テスト', () => {
    test('モック化できているか', () => {
        // spyOnすることによって、該当関数の型がspyInplementationに変化します。
        // mockReturnValueOnceによって自由にモック化できます。
        // jest.spyOnだけでは、実際の関数（モック化されていない関数）が実行されるので注意
        const walkSpy = jest.spyOn(walkModule, 'walkFast').mockReturnValueOnce('walk slow');

        expect(walkModule.walkFast()).toBe('walk slow');
        expect(walkSpy).toHaveBeenCalled();
    });
});
```

ポイントは、

- importするモジュールの割り当て
- `spyOn(モジュール変数, '関数名')`と理想的なレスポンスの実装(`mockReturnValueOnce()`)

です。
spyOnをしてレスポンスの実装をしない場合、元の関数（モック化されていない）が実行されるので注意です。

レスポンスの実装はmockReturnValueOnceだけでなく、それぞれに応じたものを選んでください。

`walkFast`関数に依存している関数を実行すると、その関数内ではモック化された`walkFast`関数が呼び出されます。



# クラス丸ごとモックにしたい

次はクラス丸ごとモック化する方法です。

```typescript:Robot_full.spec.ts

import { Robot } from './Robot';

// jest.mock()によってクラス全体をモック化できます
jest.mock('./Robot'); // パスを指定
const RobotMock = Robot as jest.Mock; // TypeScriptでは型変換する必要がある

describe('Robotのテスト', () => {
    test('クラス丸ごとモックになっているか', () => {
        // mockImplementationOnceで実装したいクラスを設定する
        RobotMock.mockImplementationOnce(() => {
            return {
                name: 'R2-D2',
                hello: (): string => {
                    return 'piro piro';
                },
            };
        });

        const robot = new Robot();
        expect(RobotMock).toHaveBeenCalled();
        expect(robot.name).toBe('R2-D2');
        expect(robot.hello()).toBe('piro piro');
    });
});
```

ポイントは、

- jest.mock()でクラス丸ごとモック化。（全てのプロパティ、メソッドがundefinedを返すようになる。）
- クラスを型変換してモック変数に代入
- `mockImplementationOnce()`で理想的なクラスを実装

です。
`jest.mock()`は`describe`などで囲むとエラーになるので、冒頭に記述しましょう。
型変換する必要があるのは、TypeScriptの型解決をするためです。Jestのリファレンスに載っていなかったので、解決に苦労しました。


# クラスの一部だけモックにしたい

最後にクラスの一部だけ、今回はメソッドだけモック化する方法です。関数をモック化する方法と基本的に同じです。

```typescript:Robot_partially.spec.ts
import { Robot } from './Robot';

// 一部だけモックにしたいので、jest.mock()はなし

describe('Robotのテスト', () => {
    test('クラスの一部だけモックになっているか', () => {
        // Robot.prototypeのhello関数をspyOnすることで、hello関数のモック化ができる
        const helloSpy = jest.spyOn(Robot.prototype, 'hello').mockReturnValue('piro piro');

        const robot = new Robot();
        expect(helloSpy).not.toHaveBeenCalled();

        expect(robot.name).toBe('C-3PO');
        expect(robot.hello()).toBe('piro piro');
        expect(helloSpy).toHaveBeenCalled();
    });
});
```

ポイントは

- jest.mock()はしない
- spyOn()の第1引数は、`クラス名.prototype`

です。
関数のモック化が理解できれば、あまり変わりなくモック化できるでしょう。

# さいごに
テストコードを作成する際に、依存先の関数とクラスをモック化する方法が分からず、リファレンスを何度も読んだり調べたりとかなり時間がかかりました。
この記事が、私と同じようにモック化の方法が分からない方の少しの助けになれれば幸いです。

それにしても、こんなに簡単にモック化できるなんてテストツールは便利ですね。

間違っている情報や表現などありましたら、遠慮なくご指摘ください。ありがとうございました。

# 参考資料
- [ES6 Class Mocks](https://jestjs.io/docs/en/es6-class-mocks)
- [Mock Functions](https://jestjs.io/docs/en/mock-functions)
- [気持ちよくUnitTestを書くために(jest)](https://qiita.com/haruharuharuby/items/2031102c40264868aec4)