测试提交按钮文本切换时出现TypeError: Cannot read properties of null (reading 'uid')
我正在测试点击提交按钮后,按钮文本是否从“Upload Image”切换为“Busy...”。但点击按钮触发图片上传流程时,出现了相关错误。
测试代码如下:
it("text change within button on load", async () => { const user = userEvent.setup(); const mockFn = jest.fn(); render(<CreateProfilePhoto handleAddData={mockFn} handleSubmit={mockFn} />); const file = new File(["hello"], "hello.png", { type: "image/png" }); const input = screen.getByTestId("upload-prompt"); await user.upload(input, file); const uploadImage = screen.getByRole("button", { name: /upload image/i }); await user.click(uploadImage); expect( screen.getByRole("button", { name: /busy.../i }) ).toBeInTheDocument(); });
错误信息:
90 | await user.upload(input, file); 91 | const uploadImage = screen.getByRole("button", { name: /upload image/i }); > 92 | await user.click(uploadImage); | ^ 93 | expect( 94 | screen.getByRole("button", { name: /busy.../i }) 95 | ).toBeInTheDocument();
以及:
Profile Photo Create › text change within button on load TypeError: Cannot read properties of null (reading 'uid') 57 | const imageRef = ref( 58 | storage, > 59 | "profileImage/" + user.uid + "/profilePhoto" | ^ 60 | ); 61 | 62 | uploadBytes(imageRef, image)
我尝试通过props传入handleSubmit,但组件未接收该props,因此没有效果。请问该错误的原因是什么?如何在单元测试范围内解决这个问题?
补充:我实际要测试的是按钮文本切换逻辑:
{isLoading ? "Busy..." : "Upload Image"}
组件完整代码:
export default function CreateProfilePhoto({ handleAddData }) { const user = auth.currentUser; const [image, setImage] = useState(); const [url, setUrl] = useState(""); const [imageID, setImageID] = useState(); const [isLoading, setIsLoading] = useState(false); const [originUrl, setOriginUrl] = useState(); //To validate image size is less than 3MB const [largeImage, setLargeImage] = useState(false); //To validate image is there before upload const [imageMissing, setImageMissing] = useState(); useEffect(() => { const work = handleAddData(imageID, url); return work; }, [imageID, url, handleAddData]); const handleImageChange = (e) => { console.log(e.target.files[0]); if (e.target.files[0]) { if (e.target.files[0].size > 3000000) { setLargeImage(true); return; } else { setLargeImage(false); setImage(e.target.files[0]); } } setImageID(e.target.id); }; const handleSubmit = () => { if (image) { setIsLoading(true); setImageMissing(false); const imageRef = ref( storage, "profileImage/" + user.uid + "/profilePhoto" ); uploadBytes(imageRef, image) .then(() => { getDownloadURL(imageRef) .then((url) => { setOriginUrl(url); setUrl(url.replace("https://firebasestorage.googleapis.com", "")); }) .catch((error) => { console.log(error.message, "download URL catch"); toast.error(error.message); }); setImage(null); setIsLoading(false); }) .catch((error) => { console.log(error.message, "upload bytes URL"); toast.error(error.message); }); } else { setImageMissing(true); } console.log(url); }; return ( <div className="m-5"> <h3>Profile Picture</h3> {largeImage && ( <p className="text-red-600 text-xs">Image must be 3MB or less</p> )} {imageMissing && ( <p className="text-red-600 text-xs"> Please attach image before upload </p> )} <img className="w-20 h-20 rounded-full ring-2 ring-orange-400 p-1 m-2" src={`appropriate_URL`} alt="Profile" /> <input type="file" onChange={handleImageChange} id="profileImageURL" accept="image/png, image/jpeg" data-testid="upload-prompt" /> <button type="button" className="border-2 p-2 hover:border-orange-400" onClick={handleSubmit} > {isLoading ? "Busy..." : "Upload Image"} </button> </div> ); }
核心问题是单元测试环境中auth.currentUser为null。组件的handleSubmit函数直接访问user.uid,但测试时没有模拟Firebase Auth的当前用户状态,导致读取null的uid属性抛出TypeError,中断了后续流程,自然无法触发isLoading状态切换,也就看不到按钮文本变化。
另外你尝试传入handleSubmit props无效,是因为组件内部并没有接收这个props,而是直接使用了自身定义的handleSubmit函数。
要在测试范围内解决问题,需要模拟Firebase Auth的currentUser,并mock掉Firebase Storage相关API(避免真实调用),确保流程能走到setIsLoading(true)这一步:
1. 模拟Firebase Auth的currentUser
在测试文件顶部添加mock代码,让auth.currentUser返回一个包含uid的模拟对象:
jest.mock('firebase/auth', () => ({ ...jest.requireActual('firebase/auth'), getAuth: () => ({ currentUser: { uid: 'test-user-123' } }) }));
2. Mock Firebase Storage相关API
避免真实调用上传逻辑,mockref、uploadBytes、getDownloadURL:
jest.mock('firebase/storage', () => ({ ref: jest.fn(() => ({})), uploadBytes: jest.fn(() => Promise.resolve()), getDownloadURL: jest.fn(() => Promise.resolve('test-url')) }));
3. 调整测试逻辑,等待状态更新
点击按钮后,状态更新是异步的,使用findByRole自动等待元素出现:
it("text change within button on load", async () => { const user = userEvent.setup(); const mockFn = jest.fn(); render(<CreateProfilePhoto handleAddData={mockFn} />); // 上传测试文件 const file = new File(["hello"], "hello.png", { type: "image/png" }); const input = screen.getByTestId("upload-prompt"); await user.upload(input, file); // 点击上传按钮 const uploadButton = screen.getByRole("button", { name: /upload image/i }); await user.click(uploadButton); // 等待按钮文本变为"Busy..." expect(await screen.findByRole("button", { name: /busy.../i })).toBeInTheDocument(); });
可选:重构组件(优化测试体验)
如果后续要更灵活地控制逻辑,可以将user通过props注入,这样测试时无需mock整个Firebase模块:
// 修改组件,接收user作为props export default function CreateProfilePhoto({ handleAddData, user }) { // 替换原有的const user = auth.currentUser; // ... 其余代码不变 }
测试时直接传入模拟user:
render(<CreateProfilePhoto handleAddData={mockFn} user={{ uid: 'test-user-123' }} />);
内容的提问来源于stack exchange,提问作者Coolkid

