首页 > 解决方案 > 我们如何在每次测试中使用 Jest 模拟依赖关系?

问题描述

这是完整的最小复制

给定以下应用程序:

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一个返回对象的函数是可以接受的 - 仍然无法让它工作)

我已经尝试过的事情:

标签: javascriptjestjsmocking

解决方案


简短的回答

设置模拟后,用于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 --showConfig可以通过查看 Jest 的缓存目录(运行以了解位置)来研究生成的代码。

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

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

ES6 import 语句必须是模块范围的,但是“good old”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.


推荐阅读