javascript - 我们如何在每次测试中使用 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
一个返回对象的函数是可以接受的 - 仍然无法让它工作)
我已经尝试过的事情:
- 通过+使用依赖注入将
Food
对象线程化getMeal
formatMeal
- (这种方法 IRL 的全部意义在于我们不想
Food
围绕整个应用程序进行线程化)
- (这种方法 IRL 的全部意义在于我们不想
- 手动模拟 +
jest.mock()
- 答案可能就在这里,但是由于导入时间的怪异,很难控制这里的值并在每次测试时重置它- 在顶部使用
jest.mock()
会为每个测试用例覆盖它,我无法弄清楚如何更改或重置Food
每个测试的值。
- 在顶部使用
解决方案
简短的回答
设置模拟后,用于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
.
推荐阅读
- algorithm - 在给定索引列表的情况下查找最接近的索引
- python - 如何将输入放入函数中
- oracle - 在 oracle ucm web center 中签入文档时发生异常
- karate - 从空手道中的 json 设置字段值
- python - 正则表达式 - 匹配后捕获单词
- python - Django - 在子路径中运行时出现静态文件的问题
- amazon-web-services - Cognito 保存用户池中的更改不起作用
- python - Python:重现 filezillaserver 密码的编码
- firebase - 使用 Flutter 向 Cloud Firestore 添加对象
- angular - Angular CLI 升级重写了我的 angular-cli.json 设置