这是完整的最小重现
给出以下应用程序:
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
Food
)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.mock('moduleName') 与 require/import 语句处于相同的范围内。
同一手册还指出:
如果您使用 ES 模块导入,那么您通常会倾向于将导入语句放在测试文件的顶部。但通常您需要在模块使用模拟之前指示 Jest 使用模拟。因此,Jest 会自动将 jest.mock 调用提升到模块的顶部(在任何导入之前)。ES6 导入在任何测试函数执行之前在模块范围内解析。因此,要应用模拟,需要在测试函数之外以及导入任何模块之前声明它们。 Jest 的 Babel 插件会将
jest.mock
语句“提升”到文件的开头,以便在发生任何导入之前执行它们。请注意,
jest.doMock
是故意不悬挂的。 可以通过查看 Jest 的缓存目录来研究生成的代码(运行
jest --showConfig
了解位置)。示例中的
food
模块很难模拟,因为它是对象文字而不是函数。最简单的方法是每次需要更改值时强制重新加载模块。选项 1a:不要使用测试中的 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!");
});
});
选项 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
中手动重置。
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!"
);
});
});
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" })
})
希望这对某人有帮助。