一部のテストでは関数をモックしたいが、他のテストではモックしたくない場合があります。異なるテストに異なるモックを提供したい場合があります。 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");
Sometimes when trying to call jest.spyOn() with a third-party package, you'll get an error like the one below:
TypeError: Cannot redefine property: useNavigate at Function.defineProperty ()
When you run into this error, you'll need to mock the package that is causing the issue:
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(); });
This code replaces the module with a Jest ES Module mock that contains all of the module's original properties using jest.mocks's factory parameter. The __esModule property is required whenever using a factory parameter in jest.mock to mock an ES module (docs).
If you wanted, you could also replace an individual function in the factory parameter. For example, React Router will throw an error if a consumer calls useNavigate() outside of a Router context, so we could use jest.mock() to replace that function throughout the whole test file if we desired:
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; }; }, }; });
I hope this information is valuable as you write your own tests. Not every app will benefit from being able to fallback to the default implementation when no implementation is provided in a test itself. Indeed, many apps will want to use the same mock for a whole testing file. However, the techniques shown in this post will give you fine-grained control over your mocking.
Let me know if I missed something or if there's something that I didn't include in this post that should be here.
以上がJest を使用して個々のテストで関数をオーバーライドするの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。