Jest测试中自定义Hook的mock函数无法在测试块外设置Promise实现
问题描述
尝试Mock自定义Hook useAuth 返回的login函数(该函数返回Promise)时遇到问题:如果在测试块内使用mockImplementation设置函数实现,功能正常;但如果在测试块外设置,调用login时会报错“cannot read then of undefined”。
附带代码:
const mockedLogin = jest.fn(() => Promise.resolve()); // 在这里设置实现无效:报错“cannot read then of undefined” jest.mock("../../hooks/useAuth", () => { return () => ({ login: mockedLogin, }); }); test("submitting the form calls onSubmit with username and password", async () => { mockedLogin.mockImplementation(() => Promise.resolve()); // 在这里设置正常工作 render(<Login />); const username = "name"; const password = "i need no password"; await userEvent.type(screen.getByLabelText(/username/i), username); await userEvent.type(screen.getByLabelText(/password/i), password); await userEvent.click(screen.getByRole("button", { name: /submit/i })); expect(mockedLogin).toHaveBeenCalledWith({ username, password, }); expect(mockedLogin).toHaveBeenCalledTimes(1); });
原因分析
Jest会自动将jest.mock()调用提升到当前模块代码的最顶部执行,优先级高于模块内所有变量的声明和赋值。这意味着当jest.mock的工厂函数运行时,你在后面声明的mockedLogin变量还没有被赋值:
- 如果用
const/let声明mockedLogin,此时变量处于暂时性死区,直接引用会抛出ReferenceError; - 如果用
var声明,变量会被初始化为undefined,导致组件拿到的login是undefined,调用时自然会报错。
而在测试块内设置mockImplementation时,mockedLogin已经被正确赋值为jest.fn实例,组件也已经获取到了这个函数引用,所以修改实现会生效。
解决方案
方案1:在mock工厂函数内部定义带实现的mock函数
这种方式彻底避免了变量提升的问题,同时通过外部变量保留对mock函数的引用,方便在测试中做断言:
// 声明一个全局可访问的变量存储mock函数 let mockedLogin; jest.mock("../../hooks/useAuth", () => { // 在工厂函数内直接创建带实现的mock函数 mockedLogin = jest.fn(() => Promise.resolve()); return () => ({ login: mockedLogin, }); }); test("submitting the form calls onSubmit with username and password", async () => { render(<Login />); const username = "name"; const password = "i need no password"; await userEvent.type(screen.getByLabelText(/username/i), username); await userEvent.type(screen.getByLabelText(/password/i), password); await userEvent.click(screen.getByRole("button", { name: /submit/i })); expect(mockedLogin).toHaveBeenCalledWith({ username, password, }); expect(mockedLogin).toHaveBeenCalledTimes(1); });
方案2:直接在mock工厂函数内定义实现,通过导入获取mock函数
如果不需要在模块全局引用mock函数,也可以直接在工厂函数内定义,然后通过导入mock后的Hook来获取login函数:
jest.mock("../../hooks/useAuth", () => ({ __esModule: true, default: () => ({ login: jest.fn(() => Promise.resolve()), }), })); // 导入mock后的自定义Hook import useAuth from "../../hooks/useAuth"; test("submitting the form calls onSubmit with username and password", async () => { // 获取mock的login函数 const mockedLogin = useAuth().login; render(<Login />); const username = "name"; const password = "i need no password"; await userEvent.type(screen.getByLabelText(/username/i), username); await userEvent.type(screen.getByLabelText(/password/i), password); await userEvent.click(screen.getByRole("button", { name: /submit/i })); expect(mockedLogin).toHaveBeenCalledWith({ username, password, }); expect(mockedLogin).toHaveBeenCalledTimes(1); });
内容的提问来源于stack exchange,提问作者Arbnor Lumezi
相关产品推荐
相关产品推荐

