我们如何在每个测试中模拟出 Jest 的依赖关系?

问题描述 投票:0回答:3

这是完整的最小重现

给出以下应用程序:

src/food.js

const Food = {
  carbs: "rice",
  veg: "green beans",
  type: "dinner"
};

export default Food;

src/food.js

import Food from "./food";

function formatMeal() {
  const { carbs, veg, type } = Food;

  if (type === "dinner") {
    return `Good evening. Dinner is ${veg} and ${carbs}. Yum!`;
  } else if (type === "breakfast") {
    return `Good morning. Breakfast is ${veg} and ${carbs}. Yum!`;
  } else {
    return "No soup for you!";
  }
}

export default function getMeal() {
  const meal = formatMeal();

  return meal;
}

我有以下测试:

_测试_/meal_test.js

import getMeal from "../src/meal";

describe("meal tests", () => {
  beforeEach(() => {
    jest.resetModules();
  });

  it("should print dinner", () => {
    expect(getMeal()).toBe(
      "Good evening. Dinner is green beans and rice. Yum!"
    );
  });

  it("should print breakfast (mocked)", () => {
    jest.doMock("../src/food", () => ({
      type: "breakfast",
      veg: "avocado",
      carbs: "toast"
    }));

    // prints out the newly mocked food!
    console.log(require("../src/food"));

    // ...but we didn't mock it in time, so this fails!
    expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!");
  });
});

如何正确模拟

Food
每个测试?换句话说,我只想为
"should print breakfast (mocked)"
测试用例应用模拟。

我也不想理想地更改应用程序源代码(尽管也许让

Food
成为一个返回对象的函数是可以接受的 - 仍然无法让它工作)

我已经尝试过的事情:

  • 通过
    Food
    线程化
    getMeal
    对象 + 使用依赖注入到
    formatMeal
    • (这种方法 IRL 的重点是我们不想在整个应用程序中线程
      Food
  • 手动模拟 +
    jest.mock()
    - 答案可能就在某个地方,但由于导入时间的怪异,很难控制此处的值并在每次测试时重置它
    • 在顶部使用
      jest.mock()
      会覆盖每个测试用例的它,并且我不知道如何更改或重置每个测试的
      Food
      的值。
javascript jestjs mocking
3个回答
19
投票

简短回答

设置模拟之后,使用
require在每个测试函数中获取新模块。

it("should print breakfast (mocked)", () => { jest.doMock(...); const getMeal = require("../src/meal").default; ... });

Food

 转换为函数,并将对 
jest.mock
 的调用放入模块作用域中。

import getMeal from "../src/meal"; import food from "../src/food"; jest.mock("../src/food"); food.mockReturnValue({ ... }); ...

长答案

Jest

手册中有一个片段,内容如下:

注意:为了正确模拟,Jest 需要 jest.mock('moduleName') 与 require/import 语句处于相同的范围内。

同一手册还指出:

如果您使用 ES 模块导入,那么您通常会倾向于将导入语句放在测试文件的顶部。但通常您需要在模块使用模拟之前指示 Jest 使用模拟。因此,Jest 会自动将 jest.mock 调用提升到模块的顶部(在任何导入之前)。

ES6 导入在任何测试函数执行之前在模块范围内解析。因此,要应用模拟,需要在测试函数之外以及导入任何模块之前声明它们。 Jest 的 Babel 插件会将

jest.mock

 语句“提升”到文件的开头,以便在发生任何导入之前执行它们。 
请注意,jest.doMock
故意不悬挂的

可以通过查看 Jest 的缓存目录来研究生成的代码(运行

jest --showConfig

 了解位置)。

示例中的

food

 模块很难模拟,因为它是对象文字而不是函数。最简单的方法是每次需要更改值时强制重新加载模块。

选项 1a:不要使用测试中的 ES6 模块

ES6 import 语句必须是模块范围的,但是“好旧的”

require

没有这样的限制,可以从测试方法的范围内调用。

describe("meal tests", () => { beforeEach(() => { jest.resetModules(); }); it("should print dinner", () => { const getMeal = require("../src/meal").default; expect(getMeal()).toBe( "Good evening. Dinner is green beans and rice. Yum!" ); }); it("should print breakfast (mocked)", () => { jest.doMock("../src/food", () => ({ type: "breakfast", veg: "avocado", carbs: "toast" })); const getMeal = require("../src/meal").default; // ...this works now expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!"); }); });

选项 1b:每次调用时重新加载模块

也可以包装被测函数。

而不是

import getMeal from "../src/meal";

使用

const getMeal = () => require("../src/meal").default();

选项 2:注册模拟并默认调用真实函数

如果

food

 模块暴露了一个函数而不是一个文字,它可能会被嘲笑。模拟实例是可变的,可以在不同的测试之间进行更改。

src/food.js

const Food = { carbs: "rice", veg: "green beans", type: "dinner" }; export default function() { return Food; }

src/meal.js

import getFood from "./food"; function formatMeal() { const { carbs, veg, type } = getFood(); if (type === "dinner") { return `Good evening. Dinner is ${veg} and ${carbs}. Yum!`; } else if (type === "breakfast") { return `Good morning. Breakfast is ${veg} and ${carbs}. Yum!`; } else { return "No soup for you!"; } } export default function getMeal() { const meal = formatMeal(); return meal; }

__tests__/meal_test.js

import getMeal from "../src/meal"; import food from "../src/food"; jest.mock("../src/food"); const realFood = jest.requireActual("../src/food").default; food.mockImplementation(realFood); describe("meal tests", () => { beforeEach(() => { jest.resetModules(); }); it("should print dinner", () => { expect(getMeal()).toBe( "Good evening. Dinner is green beans and rice. Yum!" ); }); it("should print breakfast (mocked)", () => { food.mockReturnValueOnce({ type: "breakfast", veg: "avocado", carbs: "toast" }); // ...this works now expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!"); }); });

当然,还有其他选项,例如将测试分成两个模块,其中一个文件设置模拟,另一个文件使用真实模块,或者返回一个可变对象来代替

food

 模块的默认导出,这样它就可以每次测试都会修改,然后在
beforeEach
中手动重置。


0
投票
@anttix 答案是最好的,但这里是另一个角度,在其他场景中可能有用。

babel-plugin-rewire 允许测试覆盖 import Food from "./food";

首先,

yarn add babel-plugin-rewire



babel.config.js

const presets = [ [ "@babel/env", { targets: { node: 'current', }, }, ], ]; const plugins = [ "babel-plugin-rewire" ]; module.exports = { presets, plugins };

meal_test.js

import getMeal from "../src/meal"; import Food from "../src/food"; import { __RewireAPI__ as RewireAPI } from "../src/meal"; describe("meal tests", () => { // beforeEach(() => { // jest.resetModules(); // }); afterEach(() => { RewireAPI.__Rewire__('Food', Food) }); it("should print dinner", () => { expect(getMeal()).toBe( "Good evening. Dinner is green beans and rice. Yum!" ); }); it("should print breakfast (mocked)", () => { const mockFood = { type: "breakfast", veg: "avocado", carbs: "toast" }; RewireAPI.__Rewire__('Food', mockFood) expect(getMeal()).toBe("Good morning. Breakfast is avocado and toast. Yum!"); }); it("should print dinner #2", () => { expect(getMeal()).toBe( "Good evening. Dinner is green beans and rice. Yum!" ); }); });
    

0
投票
2024 年 12 月

我知道这里有很多非常好的答案。但我发现此实现设置最快,并且易于操作多个测试的结果。

Sudo 类型和服务

这将是真实类和方法的示例。

type User = { id: string; name: string } interface Service { getUser(id: string): Promise<User | null> }

模拟服务

const mockService: Service = { getUser: jest.fn((id: string) => Promise.resolve({ id: id, name: "Jaz" })), } class MockClass { constructor(private service: Service) {} async getUser(id: string): Promise<User | null> { return await this.service.getUser(id) } }

测试

it("should return a user", async () => { const userPromise = new MockClass(mockService).getUser("123") expect(userPromise).resolves.toEqual({ id: "123", name: "Jaz" }) })
希望这对某人有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.