React组件单元测试:验证Firebase sendPasswordResetEmail调用及Mock问题
问题
我正在开发基于React和Firebase后端的Web应用,React和Firebase功能实现没问题,现在借这个项目学习React Testing Library和Jest做单元测试。目前已经能测试表单渲染、提交无效/缺失邮箱时的警告UI,但测试提交有效邮箱时Firebase的sendPasswordResetEmail是否被调用时,出现错误:TypeError: (0 , _auth.sendPasswordResetEmail)(...).then is not a function。想确认是否正确使用了jest.fn().mockResolvedValue,以及如何解决这个问题。
组件代码
import React, { useState } from "react"; import { auth } from "../../lib/firebaseApp"; import { sendPasswordResetEmail } from "firebase/auth"; const PasswordResetForm = (props) => { const [email, setEmail] = useState(null); const handleChange = (e) => { setEmail(e.target.value); }; const handleSubmit = (e) => { e.preventDefault(); sendPasswordResetEmail(auth, email) .then(() => { //Email sent }) .catch((error) => { console.log(error); }); }; return ( <form noValidate onSubmit={handleSubmit}> <input type="text" name="email" value={email || ""} onChange={handleChange} /> <SubmitButton buttonText="Reset password" /> </form> ); }; export default PasswordResetForm;
尝试的测试代码
import React from "react"; import { render, fireEvent, screen } from "@testing-library/react"; import { BrowserRouter } from "react-router-dom"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import { getAuth, sendPasswordResetEmail } from "firebase/auth"; //Component import PasswordResetForm from "../../../components/forms/passwordresetform"; //Mock firebase jest.mock('firebase/auth', () => { return { getAuth: () => jest.fn(), sendPasswordResetEmail: () => jest.fn().mockResolvedValue({}), } }); //UI tests describe("Password reset form", () => { it("Should allow good email addresses and call sendPasswordResetEmail", async () => { render(<PasswordResetForm />, { wrapper: BrowserRouter }); const input = screen.getByRole("textbox", { name: "email" }); const button = screen.getByRole("button", { name: "submit" }); const value = "user@domain.com"; userEvent.type(input, value); fireEvent.click(button); expect(sendPasswordResetEmail).toBeCalledWith(getAuth(), value); }); });
解决方案
问题根源
你的mock写法存在错误:sendPasswordResetEmail: () => jest.fn().mockResolvedValue({}) 这行代码返回的是一个函数工厂,而非直接返回Promise的mock函数。组件调用sendPasswordResetEmail(auth, email)时,实际得到的是另一个jest函数,不是Promise,自然没有.then方法,因此触发报错。
修复步骤
修正Firebase Auth的mock定义
把sendPasswordResetEmail直接定义为返回Promise的mock函数,同时调整getAuth或直接mock组件中导入的auth实例:jest.mock('firebase/auth', () => { return { sendPasswordResetEmail: jest.fn().mockResolvedValue({}), } }); // 额外mock组件中导入的auth实例,确保调用参数一致 jest.mock('../../../lib/firebaseApp', () => ({ auth: {} }));对齐测试中的断言参数
组件里使用的是从firebaseApp导入的auth实例,测试里要复用这个mock实例,避免参数不匹配:import { auth } from "../../../lib/firebaseApp"; // ... expect(sendPasswordResetEmail).toHaveBeenCalledWith(auth, value);优化测试流程(可选)
由于sendPasswordResetEmail是异步操作,建议用waitFor包裹断言,确保异步逻辑完成后再验证:await waitFor(() => { expect(sendPasswordResetEmail).toHaveBeenCalledWith(auth, value); });
完整修复后的测试代码
import React from "react"; import { render, fireEvent, screen, waitFor } from "@testing-library/react"; import { BrowserRouter } from "react-router-dom"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; import { sendPasswordResetEmail } from "firebase/auth"; import { auth } from "../../../lib/firebaseApp"; //Component import PasswordResetForm from "../../../components/forms/passwordresetform"; //Mock firebase auth jest.mock('firebase/auth', () => { return { sendPasswordResetEmail: jest.fn().mockResolvedValue({}), } }); //Mock firebaseApp里的auth实例 jest.mock('../../../lib/firebaseApp', () => ({ auth: {} })); //UI tests describe("Password reset form", () => { it("Should call sendPasswordResetEmail with valid email", async () => { render(<PasswordResetForm />, { wrapper: BrowserRouter }); const input = screen.getByRole("textbox", { name: "email" }); const button = screen.getByRole("button", { name: /reset password/i }); const testEmail = "user@domain.com"; userEvent.type(input, testEmail); fireEvent.submit(screen.getByRole('form')); await waitFor(() => { expect(sendPasswordResetEmail).toHaveBeenCalledWith(auth, testEmail); }); }); });
额外说明
- 之前测试中的按钮选择器
{ name: "submit" }不符合组件实际文本,改用正则/reset password/i匹配更可靠。 - 触发表单的
submit事件比直接点击按钮更贴近真实用户行为,避免遗漏表单的默认提交逻辑。
内容的提问来源于stack exchange,提问作者Contention

