我们如何用Jest _per test_模拟出依赖关系?

Mar*_*ark 16 javascript jestjs

(这是完整的最小代表:https://github.com/magicmark/jest_question)

鉴于以下应用程序:

SRC/food.js

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

export default Food;
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

我有以下测试:

__tests __/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!");
  });
});
Run Code Online (Sandbox Code Playgroud)

我如何正确地模拟Food 每次测试?换句话说,我只想将模拟应用于"should print breakfast (mocked)"测试用例.

我也不想理想地更改应用程序源代码(虽然可能让Food成为一个返回一个对象的函数而不是可接受的 - 但仍然无法让它工作.)

我已经尝试过的事情:

  • Food通过getMeal+使用依赖注入将对象线程化formatMeal
    • (这种方法IRL的重点是我们不想Food绕过整个应用程序)
  • 手动模拟+ jest.mock()- 这可能是答案在某处,但由于导入时间怪异,在这里控制值并在每次测试时重置它很困难
    • jest.mock()在顶部使用将覆盖每个测试用例,并且我无法确定如何更改或重置Food每个测试的值.

谢谢!

ant*_*tix 8

简短的回答

使用require抢在每个测试功能的新模块后设立嘲笑.

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

    ...
});
Run Code Online (Sandbox Code Playgroud)

要么

转Food成一个功能,把调用jest.mock到模块范围.

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

jest.mock("../src/food");
food.mockReturnValue({ ... });

...
Run Code Online (Sandbox Code Playgroud)

答案很长

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语句必须是模块作用域,但"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!");
  });
});
Run Code Online (Sandbox Code Playgroud)

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

也可以包装被测功能.

代替

import getMeal from "../src/meal";
Run Code Online (Sandbox Code Playgroud)

使用

const getMeal = () => require("../src/meal").default();
Run Code Online (Sandbox Code Playgroud)

选项2:默认情况下注册模拟并调用实际函数

如果food模块暴露了函数而不是文字,则可以模拟它.模拟实例是可变的,可以从测试更改为测试.

SRC/food.js

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

export default function() { return Food; }
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

__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!");
  });
});
Run Code Online (Sandbox Code Playgroud)

当然还有其他选项,比如将测试拆分为两个模块,其中一个文件设置模拟,另一个使用真实模块或返回可变对象代替food模块的默认导出,以便每个模块可以修改测试然后手动重置beforeEach.