Node.js (TypeScript) で class + arrow function の jest を使ったテスト

# はじめに

最近の TypeScript なら、関数は以前から存在する 'function' ではなくアロー関数を使うのが一般的だろう。しらんけど。
昨今はフロントエンドの隆盛もあってか、あるいは関数型言語からの影響か、そもそも js はクラスベースじゃないからなのか、わざわざ class にすることは少ないかもしれないが、私は日頃 c++ でなんでも class ベースで実装して暮らしているので class にまとめてしまう。そしてテストを書こうとする度に mock の使い方を忘れて調べ直すのだった。

# もくじ
* [環境](#環境)
* [class の arrow function](#class-の-arrow-function)
* [class の static な arrow function] (#class-の-static-な-arrow-function)
* [class の 非arrow function](#class-の-非arrow-function)
* [mock と元の振る舞い両方を利用したい場合](#mock-と元の振る舞い両方を利用したい場合)
* [参考](#参考)


# 環境
``` json
  "jest": "^26.6.3"
  "jest-cli": "^26.6.3"
  "ts-jest": "^26.4.4"
  "typescript": "^4.0.5"
```

# class の arrow function
* インスタンス経由で呼び出すおそらくクラスで一番よく出てくる普通の関数

``` ts
export class SoundPlayer {
    public PlaySoundFile = () => {
        ...
    }
}
```

## 結論
* jest.mock + テストで呼び出される arrow function に mock をセット

``` ts
jest.mock('SoundPlayer');

test('SoundPlayerConsumer call SoundPlayer.PlaySoundFile', () => {
    mocked(SoundPlayer).prototype.PlaySoundFile = jest.fn(() => {});
    expect(mocked(SoundPlayer).prototype.PlaySoundFile).toBeCalled();
});
```

## 説明

`jest.mock('path/to/SoundPlayer')` では arrow function は置き換わらないが、個別にモック関数を指定すれば
インスタンスメソッドを置き換える事ができる。  
ちなみに単に `jest.mock` した状態で arrow function が呼び出されると関数ではないと怒られる

``` ts
class Dog {
    public Bark = () => {
        console.log('bow wow');
    }
}
...
jest.mock('path/to/SoundPlayer');
test('it failed', () => {
    const player = new SoundPalyer('sound.mp3');
    player.PlaySoundFile(); // > TypeError: player.PlaySoundFile is not a function
})
```

クラスの prototype をモックに変えればインスタンスのメソッドが差し替わる事とイコールであることは理解できる。  
`jest.mock`により元の arrow function は消えてしまい、prototype の属性としてモックを指定していると考えればいいのだと思う




#  class の static な arrow function
``` ts
class SoundPlayer {
    public static Play = (file: string) => {
        ...
    }
}
```
## 結論
* jest.mock で OK

``` ts
jest.mock('SoundPlayer');

test('Mock Static Methode', () => {
    SoundPlayer.Play('Last Christmas');
    expect(mocked(SoundPlayer).Play).toBeCalled();
});
```
テストの後は `jest.resetAllMocks` などでモックの状態をリセットすることを忘れないように

# class の 非arrow function
* これは `jest.mock` で OK. すべての処理をモックにする必要がない場合や、単に spy 的な
機能を利用したい場合は `jest.spyOn` を使うといいだろう （メソッドについては prototype を利用する） 

``` ts
const m = jest.spyOn(SoundPlayer.prototype, 'PlaySoundFile');
```

# mock と元の振る舞い両方を利用したい場合
* import 文を使わず、import 関数を利用して都度 doMock, import を行えば可能
* 参考 https://zenn.dev/kojiroueda/articles/ad28c45bb44b2e

``` ts
    test('mocked Bark', async() => {
        jest.resetModules();

		jest.doMock('./dog'); // Dod クラスを mock 化
		const { Dog } = await import('./dog'); // import

		mocked(Dog).prototype.Bark = jest.fn(() => {
			console.log('called mocked Bark!!');
		}); // Bark の振る舞いをモックに差し替える

		const { Breeder } = await import('./breeder'); // Breeder は Dog クラスを利用する
        // Dog を mock 化した上で import することで、 Breeder は mock 化された Dog クラスを利用する
		const b = new Breeder();
		b.Show();
    }
    test('original Bark', async() => {
        jest.resetModules();
		jest.dontMock('./dog'); // mock を利用しない

        const { Breeder } = await import('./breeder');
		const b = new Breeder();
		b.Show();
    });
```
* `import()` の代わりに `require()` も利用可能

# 参考
以下の情報を参考にしました🙇‍♂️

* https://book.yyts.org/reference/import-export-require
* https://zenn.dev/kojiroueda/articles/ad28c45bb44b2e
* https://jestjs.io/ja/docs/es6-class-mocks