如何解决单元测试中的TypeError: _app.default.auth is not a function错误
TypeError: _app.default.auth is not a function
10 | setUserId(storedUserId);
11 | } else {12 | const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
仅在运行自定义Hook的单元测试时触发该错误,该Hook先从localStorage读取用户信息,若为空则从Firebase获取。报错实际来自useGetUserId Hook,但该Hook自身单元测试正常。
相关代码
useGetUserProfile Hook 代码
import { useEffect, useState } from "react"; import useGetUserId from "./useGetUserId"; import { app } from "../environments/environment"; import { doc, getDoc, getFirestore } from "firebase/firestore"; const useGetUserProfile = () => { type profile = { email: string; username: string; userBio: string; dob: Date; gender: string; sexo: string; education: string; drinkingHabits: string; smokingHabits: string; }; const db = getFirestore(app); const userId: string | null = useGetUserId(); const [isLoading, setIsLoading] = useState(true); const [userProfile, setUserProfile] = useState<any | null>(null); useEffect(() => { const userProfile = async () => { setIsLoading(true); try { const userRef = localStorage.getItem("PROFILE_INFO"); if (userRef) { const profile: profile = JSON.parse(userRef); setUserProfile(profile); } else { if (userId) { const id = JSON.parse(userId); const userRef = await getDoc(doc(db, "users", id.user.uid)); if (userRef.exists()) { const profile = userRef.data(); setUserProfile(profile); } } } } catch (error) { console.log("error", error); } finally { setIsLoading(false); } }; userProfile(); }, [setUserProfile]); return { isLoading, userProfile, setUserProfile }; }; export default useGetUserProfile;
Firebase 环境配置代码
// Import the functions you need from the SDKs you need import { initializeApp } from "firebase/app"; import { getAuth } from "firebase/auth"; // This file can be replaced during build by using the `fileReplacements` array. // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. export const firebaseConfig = { //config properties }; // Initialize Firebase export const app = initializeApp(firebaseConfig); // Initialize Firebase Authentication and get a reference to the service export const auth = getAuth(app);
单元测试代码
// Mocking Firebase Firestore methods jest.mock('firebase/firestore', () => ({ getFirestore: jest.fn(), doc: jest.fn(), getDoc: jest.fn(), })); // Mocking localStorage beforeEach(() => { jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => null); // Reset localStorage }); describe('useGetUserProfile', () => { it.only('should get user profile from localStorage if available', async () => { // Mock localStorage with a mock profile const mockProfile = { email: 'test@example.com', username: 'testuser', userBio: 'Bio info', dob: new Date('1990-01-01'), gender: 'Male', sexo: 'M', education: 'Bachelor', drinkingHabits: 'Occasionally', smokingHabits: 'Non-smoker', }; jest.spyOn(Storage.prototype, 'getItem').mockImplementationOnce(() => JSON.stringify(mockProfile)); const { result } = renderHook(() => useGetUserProfile()); expect(result.current.userProfile).toEqual(mockProfile); });
报错的 useGetUserId Hook 代码片段
useEffect(() => { const storedUserId = localStorage.getItem("USER_CREDENTIALS"); if (storedUserId) { setUserId(storedUserId); } else { const unsubscribe = firebase.auth().onAuthStateChanged((user) => { if (user) { const uid = user.uid; localStorage.setItem("USER_CREDENTIALS", uid); setUserId(uid); } }); return () => unsubscribe(); } }, [userId]); return userId; };
问题原因与解决方法
原因分析
报错核心是单元测试环境中firebase.auth()未被正确mock:当前测试仅mock了Firestore相关方法,但useGetUserId Hook会调用Firebase Auth的auth()和onAuthStateChanged方法,测试环境未提供这些mock实现,导致调用时抛出函数不存在的错误。
另外,useGetUserId Hook中直接使用firebase.auth(),但Firebase配置文件已导出初始化好的auth实例,实际代码可优化为直接导入使用,避免重复调用。
解决步骤
在单元测试中mock Firebase Auth模块
在现有测试代码中添加Firebase Auth的mock,确保auth()方法返回包含onAuthStateChanged的对象:// 新增Firebase Auth的mock jest.mock('firebase/auth', () => ({ getAuth: jest.fn(() => ({ onAuthStateChanged: jest.fn(() => jest.fn()) // 返回可取消订阅的函数 })) }));修复useGetUserId Hook的Firebase Auth调用方式
直接导入配置文件中已初始化的auth实例,避免重复调用firebase.auth():import { auth } from "../environments/environment"; // 导入已初始化的auth实例 // ... 其他代码 useEffect(() => { const storedUserId = localStorage.getItem("USER_CREDENTIALS"); if (storedUserId) { setUserId(storedUserId); } else { // 使用导入的auth实例,而非firebase.auth() const unsubscribe = auth.onAuthStateChanged((user) => { if (user) { const uid = user.uid; localStorage.setItem("USER_CREDENTIALS", uid); setUserId(uid); } }); return () => unsubscribe(); } }, [userId]);确保localStorage的mock覆盖所有情况
当前测试仅mock了PROFILE_INFO,但useGetUserId会读取USER_CREDENTIALS,可在测试中同时mock该值,避免进入Firebase Auth分支:it.only('should get user profile from localStorage if available', async () => { // Mock localStorage with a mock profile const mockProfile = { email: 'test@example.com', username: 'testuser', userBio: 'Bio info', dob: new Date('1990-01-01'), gender: 'Male', sexo: 'M', education: 'Bachelor', drinkingHabits: 'Occasionally', smokingHabits: 'Non-smoker', }; // 同时mock USER_CREDENTIALS,避免触发Firebase Auth逻辑 jest.spyOn(Storage.prototype, 'getItem') .mockImplementation((key) => { if (key === 'PROFILE_INFO') return JSON.stringify(mockProfile); if (key === 'USER_CREDENTIALS') return 'mock-uid'; return null; }); const { result } = renderHook(() => useGetUserProfile()); expect(result.current.userProfile).toEqual(mockProfile); });
内容的提问来源于stack exchange,提问作者D.Hodges

