「労働者が自分の仕事をうまくやりたいなら、まず自分の道具を研ぎ澄まさなければなりません。」 - 孔子、「論語。陸霊公」
表紙 > プログラミング > Jest を使用して個々のテストで関数をオーバーライドする

Jest を使用して個々のテストで関数をオーバーライドする

2024 年 11 月 2 日に公開
ブラウズ:577

Override functions in individual tests using Jest

一部のテストでは関数をモックしたいが、他のテストではモックしたくない場合があります。異なるテストに異なるモックを提供したい場合があります。 Jest はこれを厄介にします。そのデフォルトの動作は、単一のテストだけでなく、テスト ファイル全体のパッケージの関数をオーバーライドすることです。 Python の @patch や Laravel のサービス コンテナなどの柔軟なツールを使用したことがある場合、これは奇妙に思えます。

この投稿では、個々のテストで関数をモックし、モックが提供されていない場合は元の実装にフォールバックする方法を説明します。 CommonJS モジュールと ES モジュールの両方の例が示されます。この投稿で説明した手法は、ファーストパーティのモジュールとサードパーティのパッケージの両方で機能します。

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 ファイルは、以下に示すようなオブジェクト構文を使用してモジュールをエクスポートします。

// 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() 関数に渡すことができます。その後、独自の戻り値または実装を提供したいテストは、テスト本体内で手動でそれを行うことができます。

オブジェクトのエクスポート時に CommonJS をモックする

上記のコードが単一の関数ではなくオブジェクトをエクスポートしたとしましょう:

// 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 モジュールのモック化

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");

ES モジュールのインポートのトラブルシューティング

サードパーティのパッケージで 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;
      };
    },
  };
});

まとめ

この情報が独自のテストを作成する際に役立つことを願っています。テスト自体に実装が提供されていない場合、すべてのアプリがデフォルトの実装にフォールバックできるメリットがあるわけではありません。実際、多くのアプリはテスト ファイル全体に同じモックを使用したいと考えます。ただし、この投稿で示されているテクニックを使用すると、モックをきめ細かく制御できるようになります。

何か見逃している場合、またはこの投稿に記載していないものがここにあるはずである場合は、お知らせください。

リリースステートメント この記事は次の場所に転載されています: https://dev.to/tylerlwsmith/override-functions-in-individual-tests-using-jest-dp5?1 侵害がある場合は、[email protected] に連絡して削除してください。
最新のチュートリアル もっと>

免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。

Copyright© 2022 湘ICP备2022001581号-3