依存クラスの一部分だけをモックにして、jestでユニットテストをする方法
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つのソースコードがあるとします。
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"}
export class UserRepository {
private _adapter: UserApiAdapter;
constructor({ adapter }: { adapter: UserApiAdapter }) {
this._adapter = adapter;
}
async find(id: number) {
return this._adapter.getUser(1);
}
}
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
の挙動を色々な方法で変えていってみたいと思います。
ユニットテスト
まずはテストコードを書いてみます。
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
のユニットテストとしては良く無いです。また、依存しているクラスのメソッドの挙動を自由に変えられないのでテストケースを思うように追加できません。
依存をテスト対象に注入できる場合
以下のようにすると一部の挙動だけをモックに変えたインスタンスを生成し、それをテスト対象に注入してユニットテストを書くことができます。
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を考えられていない時代のソースコードなどの場合、テスト対象のクラスが依存を注入できるようになっていない場合があります。その場合は無理やりオブジェクトを書き換えればテストできます。
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",
});
});
完全なコードのサンプル
完全なコードのサンプルはこちらにあります。requireActual
した後のモジュールのstatic変数を書き換えるサンプルもあります。
テストが整備されたソースコードが世の中に増えると良いなと思います。
Discussion
UserApiAdapterではないですか?
英語的にはどっちでも良いはずですが、Adapterのほうがしっくりくるので直しておきます。