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.jsautomocktrue にセットしてあげると、それぞれのファイルで自動的に __mocks__ にあるモジュールを読み込んでテストを実行してくれるので、それがメリットかもしれません。

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

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

階層イメージはこのとおりです(出典は 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 の Node.js 向け package の mock を例にします。cloudinary.ts というファイルを作成しました。

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があり、それに uploaddestroy というメソッドがある

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

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

なお補足ですが、

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

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

mock を発動する

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

jest.mock('cloudinary');

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

jest の公式マニュアルにある Manual Mocks にある内容に準じます。

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

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

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

この例では aws-sdkDynamoDB クラスを 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 の中を見てどういうアウトプットを返すのかを見た上で実装しています。

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 ではない状態に戻してあげましょう。各テストケース実行前に状態をクリアしないと、関数を何回呼び出しかどうかの記録が積み上がってしまいます(一回しか実行していないはずなのに2回3回となる)。


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

まとめ

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

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

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