首頁  >  文章  >  web前端  >  使用 Jest 覆蓋各個測試中的函數

使用 Jest 覆蓋各個測試中的函數

WBOY
WBOY原創
2024-08-05 22:43:02644瀏覽

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 開發人員也使用它們。然而,CommonJS 仍然是 Node 的預設值。無論您使用哪個系統,都值得閱讀整篇文章來了解 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") 時,這會用自動模擬(文件)取代模組(文件及其所有導出)。當呼叫自動模擬時,它將返回未定義。但是,它將提供用於覆蓋模擬的實作、返回值等的方法。您可以在 Jest Mock Functions 文件中查看它提供的所有屬性和方法。

我們可以使用mock的mockImplementation()方法自動將mock的實作設定為原始模組的實作。 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");

Troubleshooting ES module imports

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 (<anonymous>)

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;
      };
    },
  };
});

Wrapping up

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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn