🙆

依存クラスの一部分だけをモックにして、jestでユニットテストをする方法

2020/10/17に公開
2

TL;DR

jest.requireActual("<moduleのpath>") を使うとモジュールの一部だけをモックにすることができます。
参考URL: https://jestjs.io/docs/ja/jest-object#jestrequireactualmodulename
完全なコードのサンプルは https://github.com/kn3ny/jest-playground にも上げてあります。

なぜ一部分だけモックにしたいのか

ユニットテストでは、テスト対象のクラスが依存しているクラスをモックに置き換えてテストをすることになりますが、モックを一から全て定義するのは面倒です。また、外部APIへ接続するメソッドだけ挙動を変えてユニットテストをしたい、というのも良くあるケースだと思います。

そういう時に jest.requireActual("<moduleのpath>") を使うと、苦痛なくテストを書けます。

前提のコード

以下の3つのソースコードがあるとします。

src/index.ts
import { UserApiAdapter } from "~/infrastructure/userApiAdapter";
import { UserRepository } from "~/repository/userRepository";

const userRepository = new UserRepository({
  adapter: new UserApiAdapter(),
});

const user = await userRepository.find(1);
// => {"id": 1, "name": "Real name"}
src/repository/userRepository.ts
export class UserRepository {
  private _adapter: UserApiAdapter;

  constructor({ adapter }: { adapter: UserApiAdapter }) {
    this._adapter = adapter;
  }

  async find(id: number) {
    return this._adapter.getUser(1);
  }
}
src/infrastructure/userApiAdapter.ts
export class UserApiAdapter {
  private static readonly DUMMY_DATA = {
    id: 1,
    name: "Real name",
  };

  async getUser(id: number) {
    // 実際はここでAPIコール
    return Promise.resolve(UserApiAdapter.DUMMY_DATA);
  }
}

この状態で、UserRepositoryのユニットテストを書き、UserRepositoryが依存しているUserApiAdapterの挙動を色々な方法で変えていってみたいと思います。

ユニットテスト

まずはテストコードを書いてみます。

__tests__/repository/userRepository.test.ts
import { UserApiAdapter } from "~/infrastructure/userApiAdapter";
import { UserRepository } from "~/repository/userRepository";

describe("UserRepository", () => {
  test("find()", async () => {
    const repo = new UserRepository({
      adapter: new UserApiAdapter(),
    });

    await expect(repo.find(1)).resolves.toStrictEqual({
      id: 1,
      name: "Real name",
    });
  });
});

これは本物のUserApiAdapterを使ってしまっていて、UserRepositoryのユニットテストとしては良く無いです。また、依存しているクラスのメソッドの挙動を自由に変えられないのでテストケースを思うように追加できません。

依存をテスト対象に注入できる場合

以下のようにすると一部の挙動だけをモックに変えたインスタンスを生成し、それをテスト対象に注入してユニットテストを書くことができます。

__tests__/repository/userRepository.test.ts の一部
  test("find()", async () => {
    // jest.requireActual(...) => { UserApiAdapter: ... } が返って来ます。
    // 名前を区別するために分割代入でUserApiAdapterをMockedUserApiAdapterとして取り出します
    const { UserApiAdapter: MockedUserApiAdapter } = jest.requireActual(
      "~/infrastructure/userApiAdapter"
    );

    // MockedUserApiAdapterを使ってUserApiAdapterのインスタンスを作ります
    const mockedUserApiAdapter: UserApiAdapter = new MockedUserApiAdapter();
    // UserApiAdapterのgetUserメソッドのみモックにします
    mockedUserApiAdapter.getUser = jest.fn().mockImplementation(async () => {
      return Promise.resolve({
        id: 2,
        name: "Mocked name",
      });
    });
    
    // getUserメソッドのみモックにしたUserApiAdapterを注入します
    const repo = new UserRepository({
      adapter: mockedUserApiAdapter,
    });

    // そうすると、モックにした値に変わります。
    await expect(repo.find(1)).resolves.toStrictEqual({
      id: 2,
      name: "Mocked name",
    });
    
    // モックなので、呼ばれた回数や呼ばれた引数を検証することもできます
    expect(mockedUserApiAdapter.getUser).toBeCalledTimes(1);
    expect(mockedUserApiAdapter.getUser).toBeCalledWith(1);
  });

依存をテスト対象に注入できない場合

DIを考えられていない時代のソースコードなどの場合、テスト対象のクラスが依存を注入できるようになっていない場合があります。その場合は無理やりオブジェクトを書き換えればテストできます。

__tests__/repository/userRepository.test.ts の一部
  test("find()", async () => {
    // mockedUserApiAdapterのセットアップは上と同じなので説明は省略
    const { UserApiAdapter: MockedUserApiAdapter } = jest.requireActual(
      "~/infrastructure/userApiAdapter"
    );

    const mockedUserApiAdapter: UserApiAdapter = new MockedUserApiAdapter();
    mockedUserApiAdapter.getUser = jest.fn().mockImplementation(async () => {
      return Promise.resolve({
        id: 4,
        name: "Force-mocked name",
      });
    });

    const repo = new UserRepository({
      adapter: new UserApiAdapter(),
    });

    // オブジェクトの書き換え。必要に応じてts-ignoreなどをする必要があります
    repo._adapter = mockedUserApiAdapter;

    // モックにした値で検証できます
    await expect(repo.find(1)).resolves.toStrictEqual({
      id: 4,
      name: "Force-mocked name",
    });
  });

完全なコードのサンプル

完全なコードのサンプルはこちらにあります。
https://github.com/kn3ny/jest-playground
これまで上げた例の他に、requireActualした後のモジュールのstatic変数を書き換えるサンプルもあります。
テストが整備されたソースコードが世の中に増えると良いなと思います。

Discussion

KennyKenny

英語的にはどっちでも良いはずですが、Adapterのほうがしっくりくるので直しておきます。