一部のテストでは関数をモックしたいが、他のテストではモックしたくない場合があります。異なるテストに異なるモックを提供したい場合があります。 Jest はこれを厄介にします。そのデフォルトの動作は、単一のテストだけでなく、テスト ファイル全体のパッケージの関数をオーバーライドすることです。 Python の @patch や Laravel のサービス コンテナなどの柔軟なツールを使用したことがある場合、これは奇妙に思えます。
この投稿では、個々のテストで関数をモックし、モックが提供されていない場合は元の実装にフォールバックする方法を説明します。 CommonJS モジュールと ES モジュールの両方の例が示されます。この投稿で説明した手法は、ファーストパーティのモジュールとサードパーティのパッケージの両方で機能します。
この投稿では複数のモジュール システムについて説明するため、それらが何であるかを理解することが重要です。
CommonJS (略称 CJS) は Node.js のモジュール システムです。 module.exports を使用して関数をエクスポートし、require():
を使用して関数をインポートします。
// CommonJS export function greet() { return "Hello, world!"; } module.exports = { greet };
// CommonJS import const getUsersList = require('./greet');
ES モジュール (ESM の略) は、ブラウザで使用されるモジュール システムです。これは、export キーワードを使用して関数をエクスポートし、import キーワードを使用して関数をインポートします:
// ES module export export default function greet() { return "Hello, world!"; }
// ES module import import { greet } from "./greet";
この記事の執筆時点では、ほとんどのフロントエンド JavaScript 開発者が ES モジュールを使用しており、多くのサーバーサイド JS 開発者も同様に ES モジュールを使用しています。ただし、Node のデフォルトは依然として CommonJS です。どのシステムを使用するかに関係なく、Jest のモッキング システムについて学ぶために記事全体を読む価値があります。
通常、CommonJS ファイルは、以下に示すようなオブジェクト構文を使用してモジュールをエクスポートします。
// CommonJS export function greet() { return "Hello, world!"; } module.exports = { greet: greet };
ただし、関数を単独でエクスポートすることも可能です:
// CommonJS export function greet() { return "Hello, world!"; } module.exports = greet;
これを独自のコードで行うことは必ずしもお勧めしません。 オブジェクトをエクスポートすると、アプリケーションの開発中に頭の痛い問題が軽減されます。ただし、これはよくあることなので、CommonJS でエクスポートされた裸の関数をモックし、テストが独自の実装を提供しない場合は元の関数にフォールバックする方法を議論する価値があります。
テスト中にモックしたい次の CommonJS ファイルがあるとします:
// cjsFunction.js function testFunc() { return "original"; } module.exports = testFunc;
次のコードを使用してテストでモックできます:
const testFunc = require("./cjsFunction"); jest.mock("./cjsFunction"); beforeEach(() => { testFunc.mockImplementation(jest.requireActual("./cjsFunction")); }); it("can override the implementation for a single test", () => { testFunc.mockImplementation(() => "mock implementation"); expect(testFunc()).toBe("mock implementation"); expect(testFunc.mock.calls).toHaveLength(1); }); it("can override the return value for a single test", () => { testFunc.mockReturnValue("mock return value"); expect(testFunc()).toBe("mock return value"); expect(testFunc.mock.calls).toHaveLength(1); }); it("returns the original implementation when no overrides exist", () => { expect(testFunc()).toBe("original"); expect(testFunc.mock.calls).toHaveLength(1); });
jest.mock("./cjsFunction") を呼び出すと、モジュール (ファイルとそのすべてのエクスポート) が自動モック (ドキュメント) に置き換えられます。 auto-mock が呼び出されると、unknown が返されます。ただし、モックの実装、戻り値などをオーバーライドするためのメソッドが提供されます。 Jest Mock Functions のドキュメントで、Jest Mock Functions が提供するすべてのプロパティとメソッドを確認できます。
モックのmockImplementation()メソッドを使用して、モックの実装を元のモジュールの実装に自動的に設定できます。 Jest は、現在モックされている場合でも、常に元のモジュールをロードする jest.requireActual() メソッドを提供します。
モック実装と戻り値は各テスト後に自動的にクリアされるため、各テストの前にモックの実装を元の実装に設定するコールバック関数を Jest の beforeEach() 関数に渡すことができます。その後、独自の戻り値または実装を提供したいテストは、テスト本体内で手動でそれを行うことができます。
上記のコードが単一の関数ではなくオブジェクトをエクスポートしたとしましょう:
// cjsModule.js function testFunc() { return "original"; } module.exports = { testFunc: testFunc, };
テストは次のようになります:
const cjsModule = require("./cjsModule"); afterEach(() => { jest.restoreAllMocks(); }); it("can override the implementation for a single test", () => { jest .spyOn(cjsModule, "testFunc") .mockImplementation(() => "mock implementation"); expect(cjsModule.testFunc()).toBe("mock implementation"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1); }); it("can override the return value for a single test", () => { jest.spyOn(cjsModule, "testFunc").mockReturnValue("mock return value"); expect(cjsModule.testFunc()).toBe("mock return value"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1); }); it("returns the original implementation when no overrides exist", () => { expect(cjsModule.testFunc()).toBe("original"); }); it("can spy on calls while keeping the original implementation", () => { jest.spyOn(cjsModule, "testFunc"); expect(cjsModule.testFunc()).toBe("original"); expect(cjsModule.testFunc.mock.calls).toHaveLength(1); });
jest.spyOn() メソッドを使用すると、Jest がオブジェクトのメソッドへの呼び出しを記録し、独自の置換を提供できるようになります。この のみ はオブジェクトに対して機能します。モジュールは関数を含むオブジェクトをエクスポートしているため、これを使用できます。
spyOn() メソッドはモックであるため、その状態をリセットする必要があります。 Jest spyOn() ドキュメントでは、afterEach() コールバックで jest.restoreAllMocks() を使用して状態をリセットすることを推奨しています。これは上記で行ったことです。これを行わなかった場合、モックは spyOn() が呼び出された後の次のテストで未定義を返します。
ES モジュールにはデフォルトのエクスポートと名前付きエクスポートを含めることができます:
// esmModule.js export default function () { return "original default"; } export function named() { return "original named"; }
上記のファイルのテストは次のようになります:
import * as esmModule from "./esmModule"; afterEach(() => { jest.restoreAllMocks(); }); it("can override the implementation for a single test", () => { jest .spyOn(esmModule, "default") .mockImplementation(() => "mock implementation default"); jest .spyOn(esmModule, "named") .mockImplementation(() => "mock implementation named"); expect(esmModule.default()).toBe("mock implementation default"); expect(esmModule.named()).toBe("mock implementation named"); expect(esmModule.default.mock.calls).toHaveLength(1); expect(esmModule.named.mock.calls).toHaveLength(1); }); it("can override the return value for a single test", () => { jest.spyOn(esmModule, "default").mockReturnValue("mock return value default"); jest.spyOn(esmModule, "named").mockReturnValue("mock return value named"); expect(esmModule.default()).toBe("mock return value default"); expect(esmModule.named()).toBe("mock return value named"); expect(esmModule.default.mock.calls).toHaveLength(1); expect(esmModule.named.mock.calls).toHaveLength(1); }); it("returns the original implementation when no overrides exist", () => { expect(esmModule.default()).toBe("original default"); expect(esmModule.named()).toBe("original named"); });
これは、前の CommonJS の例とほぼ似ていますが、いくつかの重要な違いがあります。
まず、モジュールを名前空間インポートとしてインポートします。
import * as esmModule from "./esmModule";
次に、デフォルトのエクスポートをスパイしたい場合は、「default」を使用します:
jest .spyOn(esmModule, "default") .mockImplementation(() => "mock implementation default");
サードパーティのパッケージで jest.spyOn() を呼び出そうとすると、次のようなエラーが発生することがあります:
TypeError: Cannot redefine property: useNavigate at Function.defineProperty ()
このエラーが発生した場合は、問題の原因となっているパッケージをモックする必要があります:
import * as reactRouterDOM from "react-router-dom"; // ADD THIS: jest.mock("react-router-dom", () => { const originalModule = jest.requireActual("react-router-dom"); return { __esModule: true, ...originalModule, }; }); afterEach(() => { jest.restoreAllMocks(); });
このコードは、jest.mocks のファクトリー パラメーターを使用して、モジュールの元のプロパティをすべて含む Jest ES モジュール モックでモジュールを置き換えます。 __esModule プロパティは、jest.mock のファクトリ パラメータを使用して ES モジュール (ドキュメント) をモックする場合は常に必要です。
必要に応じて、ファクトリパラメータの個々の関数を置き換えることもできます。たとえば、コンシューマが Router コンテキストの外で useNavigate() を呼び出した場合、React Router はエラーをスローするため、必要に応じて jest.mock() を使用してテスト ファイル全体でその関数を置き換えることができます:
jest.mock("react-router-dom", () => { const originalModule = jest.requireActual("react-router-dom"); return { __esModule: true, ...originalModule, // Dummy that does nothing. useNavigate() { return function navigate(_location) { return; }; }, }; });
この情報が独自のテストを作成する際に役立つことを願っています。テスト自体に実装が提供されていない場合、すべてのアプリがデフォルトの実装にフォールバックできるメリットがあるわけではありません。実際、多くのアプリはテスト ファイル全体に同じモックを使用したいと考えます。ただし、この投稿で示されているテクニックを使用すると、モックをきめ細かく制御できるようになります。
何か見逃している場合、またはこの投稿に記載していないものがここにあるはずである場合は、お知らせください。
免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。
Copyright© 2022 湘ICP备2022001581号-3