jest で package を mock する

# Motivation

テストするときには実際にAWSにアクセスさせずに Unit testing や Integration testing をしたい場合があります。この場合に使うのが Mocking ですが、 `jest` での Mocking のやり方を整理します。

# package ごと Mocking する方法は二つある

package ごと Mocking する方法は以下の二つがあります。
1. `__mocks__` というディレクトリを作ってそこに Mocking した処理を書く
2. テストファイルごとに Mocking した処理を書く

package を包括的に mock する場合は1, packageの中のメソッドを spying する場合は2で実装します。

## `__mocks__` を使う場合

こちらを使う場合は `jest.config.js` で `automock` を `true` にセットしてあげると、それぞれのファイルで自動的に `__mocks__` にあるモジュールを読み込んでテストを実行してくれるので、それがメリットかもしれません。

### 1. `__mocks__` という名のディレクトリを作成する

Mockしたい対象同じ階層に `__mocks__` というディレクトリを作成します。小文字でないといけませんし、`__mock__` という単数形でも動作しませんのでご注意ください。
この記事では package を mock するという記事ですので、`node_modules` と同じ階層にディレクトリを作成することになります。

階層イメージはこのとおりです（出典は [Manual Mocks](https://jestjs.io/docs/en/manual-mocks) ）。この例では `node_modules` にある `fs` という package の他に `models` というディレクトリにある `user.js` を mock しようとしている例です。

```
.
├── config
├── __mocks__
│   └── fs.js
├── models
│   ├── __mocks__
│   │   └── user.js
│   └── user.js
├── node_modules
└── views
```

### 2. `__mocks__` というディレクトリに mock ファイルを作成する

mock する対象をファイル名にしてファイルを作成します。この例では [cloudinary](https://cloudinary.com/documentation/node_integration) の Node.js 向け package の mock を例にします。`cloudinary.ts` というファイルを作成しました。

```js
import { createPhotos } from '@Test/createPhotos';
const cloudinary = jest.genMockFromModule('cloudinary');

const { firstPhotoInClourinaryMock } = createPhotos();

const v2 = {
  config: ({  }: { cloud_name: string; api_key: string; api_secret: string }) => {},
  uploader: {
    upload: (url: string, _: any, callback) => {
      if (url === 'error') {
        callback(new Error('Error'), null);
      } else {
        callback(null, firstPhotoInClourinaryMock);
      }
    },
    destroy: (id: string, callback) => {
      if (id === 'error') {
        callback(new Error('Error'), null);
      } else {
        callback(null, { result: 'success' });
      }
    },
  },
};

// @ts-ignore
cloudinary.v2 = v2;

module.exports = cloudinary;
```

#### jest.genMockFromModule

このメソッドによって package を mock することができます。`genMockFromModule` を使わずに完全にカスタムでも mock は作れますが、使わない理由がここではないので使いました。このメソッドによって、元々 `cloudinary` のモジュールにあった処理は上書きされます。

#### cloudinary の処理を mock する

`cloudinary` の使い方を見ると以下がわかります。
1. `cloudinary` は object で `v2` という object がある
2. その object の中に `config`というメソッドがある
3. 1の object の中に `uploader` というobjectがあり、それに `upload` と `destroy` というメソッドがある

```js
cloudinary.v2.config()
cloudinary.v2.uploader.upload()
cloudinary.v2.uploader.destroy()
```

そこで、mock では `v2` という objectを作り、それに上記でわかった1, 2, 3を実装しました。それぞれのメソッドの使い方を見て、実際のモジュールの動きをさせつつ、テストデータを伴ってコールバックを実行する必要がある場合にはコールバックを実行させるようにしています。

なお補足ですが、

```js
import { createPhotos } from '@Test/createPhotos';
const { firstPhotoInClourinaryMock } = createPhotos();
```

これは自前で作っているテストデータを作成する関数です。

#### mock を発動する

あとはテストファイルで以下を冒頭に書いてあげてください。

```js
jest.mock('cloudinary');
```

これで `cloudinary` の mock したモジュールが読み込まれます。または `jest.config.js` で `automock` を `true` にセットしてあげると、それぞれのファイルで自動的に `__mocks__` にあるモジュールを読み込んでテストを実行してくれます。

jest の公式マニュアルにある [Manual Mocks](https://jestjs.io/docs/en/manual-mocks) にある内容に準じます。

## テストファイルの中で mock を作成する

テストファイルの中で `jest.mock()` を使って mock することもできます。package のあるメソッドを spying したい場合はこちらで実装します。

### テストファイルの冒頭で mock を作成する

この例では `aws-sdk` の `DynamoDB` クラスを mock してみます。`DynamoDB.DocumentClient` のインスタンスを生成して `DynamoDB` にアクセスする処理を持っているとします。

#### 1. package の使い方を見ながら mock を作る

`aws-sdk` の使い方を見ると `aws-sdk` 自体が object であり、`DynamoDB` はクラスであり、またその中にある `DocumentClient` もまたクラスであることがわかります。JavaScriptではクラスは function そのものなので、ネストした object を mock として作りこんであげます。

また mock する前に `mockPut` という変数を作って `jest.fn()` のアウトプットを入れているのは、 spying するためです。これによって何回この mock した関数が呼ばれたか（`toHaveBeenCalledTimes`）とか、意図した引数と共に関数が呼ばれたか（`toHaveBeenCalledWith`）どうかをテストできます。

また mock した put メソッドのアウトプットは `aws-sdk` のドキュメントや `node_modules` の中を見てどういうアウトプットを返すのかを見た上で実装しています。

```js
import { DynamoDB } from 'aws-sdk';

const mockPut = jest.fn();
jest.mock('aws-sdk', () => {
  return {
    DynamoDB: {
      DocumentClient: jest.fn(() => {
        return {
          put: mockPut.mockImplementation(() => {
            return {
              promise: async (): Promise<DynamoDB.DocumentClient.PutItemOutput> => ({
                Attributes: undefined,
                ConsumedCapacity: undefined,
                ItemCollectionMetrics: undefined,
              }),
            };
          }),
        };
      }),
    },
  };
});
```

#### 2. テスト実行前に clear, テスト実行後に restore

必要に応じてテスト実行前に mock の状態をクリアしたり、テストが終わった後に mock ではない状態に戻してあげましょう。各テストケース実行前に状態をクリアしないと、関数を何回呼び出しかどうかの記録が積み上がってしまいます（一回しか実行していないはずなのに２回３回となる）。

```js

  beforeEach(() => {
    mockPut.mockClear();
  });
```

# まとめ

再掲します。
package ごと Mocking する方法は以下の二つがあります。
1. `__mocks__` というディレクトリを作ってそこに Mocking した処理を書く
2. テストファイルごとに Mocking した処理を書く

package を包括的に mock する場合は1, packageの中のメソッドを spying する場合は2で実装します。

ただしテストを書いているとやはり package のメソッドを spying したいので2で各パターンが僕は圧倒的に多いです。jest の `spyOn` というのもあるので、 spying に関してまた別の記事で書きたいと思います。