(以下是完整的最小复制:https://github.com/magicmark/jest_question)
给出以下应用程序:
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;
}
我进行了以下测试:
__ 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!");
});
});
如何正确模拟Food
每次测试?换句话说,我只想将模拟应用于"should print breakfast (mocked)"
测试用例。
我也不想改变应用程序的源代码(尽管也许 Food 是一个返回对象的函数是可以接受的-仍然不能使它正常工作。 )
我已经尝试过的事情:
Food
绕过getMeal
对象,并使用依赖性注入formatMeal
中
Food
线程)jest.mock()
-答案可能在某处,但是由于导入时间怪异,很难在此处控制值并在每次测试中将其重置
jest.mock()
会覆盖每个测试用例,而我不知道如何在每次测试中更改或重置Food
的值。 谢谢!
答案 0 :(得分:8)
设置模拟后, 后,使用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 manual中有一个片段,内容为:
注意:为了正确模拟,Jest需要jest.mock('moduleName')与require / import语句处于同一范围。
same manual还指出:
在执行任何测试功能之前,在模块范围内解析如果您正在使用ES模块导入,则通常倾向于将import语句放在测试文件的顶部。但是通常您需要指示Jest在模块使用模拟之前使用它。因此,Jest会自动将jest.mock调用提升到模块的顶部(在导入之前)。
ES6导入。因此,要应用模拟,需要在测试函数外部和导入任何模块之前声明它们。 Jest的Babel插件会将jest.mock
语句“提升”到文件的开头,以便在导入之前执行它们。 请注意,jest.doMock
是deliberately not hoisted。
可以通过窥视Jest的缓存目录(运行jest --showConfig
来了解位置)来研究生成的代码。
示例中的food
模块很难模拟,因为它是对象文字而不是函数。最简单的方法是在每次需要更改值时强制重新加载模块。
ES6导入语句必须在模块范围内,但是“好旧的” 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!");
});
});
也可以包装被测函数。
代替
import getMeal from "../src/meal";
使用
const getMeal = () => require("../src/meal").default();
如果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
中手动重置。
答案 1 :(得分: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!"
);
});
});