有时您想在某些测试中模拟函数,但不想在其他测试中模拟函数。有时您想为不同的测试提供不同的模拟。 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 开发人员也使用它们。然而,CommonJS 仍然是 Node 的默认设置。无论您使用哪个系统,都值得阅读整篇文章来了解 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") 时,这会用自动模拟(文档)替换模块(文件及其所有导出)。当调用自动模拟时,它将返回未定义。但是,它将提供用于覆盖模拟的实现、返回值等的方法。您可以在 Jest Mock Functions 文档中查看它提供的所有属性和方法。
我们可以使用mock的mockImplementation()方法自动将mock的实现设置为原始模块的实现。 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 (<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; }; }, }; });
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中文网其他相关文章!