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

公開:2020/10/17
更新:2020/10/17
4 min読了の目安(約4300字TECH技術記事

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 { UserApiAdaptor } from "~/infrastructure/userApiAdaptor";
import { UserRepository } from "~/repository/userRepository";

const userRepository = new UserRepository({
  adaptor: new UserApiAdaptor(),
});

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

  constructor({ adaptor }: { adaptor: UserApiAdaptor }) {
    this._adaptor = adaptor;
  }

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

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

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

ユニットテスト

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    const repo = new UserRepository({
      adaptor: new UserApiAdaptor(),
    });

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

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

完全なコードのサンプル

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