如何修复Jest中Cloud Functions的Firestore单元测试状态不一致问题
我按照Cloud Functions的规范编写了在线模式的单元测试,调试时遇到了状态不一致的问题。我尝试通过重置并初始化(seed)Firestore数据库来保证测试前的初始状态一致,单独运行每个测试套件时一切正常,但同时运行所有套件会出现大量错误,我确定这和beforeEach钩子的执行逻辑有关,但不知道如何修复。
我共有3个测试套件,以下是其中一个的代码:
// eslint-disable-next-line spaced-comment /// <reference types="jest" /> import { cleanup, getDb, initializeFirebase, makeChange, resetDb, wrap } from './util'; initializeFirebase(); import { streamerIsOffline, streamerIsOnline } from './__fixtures__/onStreamerUpdateSnaps'; import { getScheduler, SchedulerClientWrapper } from './../utils/SchedulerClientWrapper'; import onStreamerUpdate from '../onStreamerUpdate'; const db = getDb(); jest.mock('./../utils/SchedulerClientWrapper'); const mockedGetScheduler = jest.mocked(getScheduler, true); const wrapped = wrap(onStreamerUpdate); const schedulerClientWrapper = new SchedulerClientWrapper(); const mockedPause: jest.Mock = jest.fn(); const mockedResume: jest.Mock = jest.fn(); const mockedJobIsEnabled: jest.Mock = jest.fn(); schedulerClientWrapper.pause = mockedPause; schedulerClientWrapper.resume = mockedResume; schedulerClientWrapper.jobIsEnabled = mockedJobIsEnabled; describe('onStreamerUpdate', () => { beforeEach( async () => { await resetDb(); // 这里尝试重置Firestore mockedGetScheduler.mockClear(); jest.resetAllMocks(); }); it('should resume the scheduledJob when the first streamer becomes online', async () => { await updateStreamerStatus('64522496', true); // 准备测试前置数据 const beforeSnap = streamerIsOffline; const afterSnap = streamerIsOnline; const change = makeChange(beforeSnap, afterSnap); mockedGetScheduler.mockReturnValue(schedulerClientWrapper); mockedJobIsEnabled.mockResolvedValue(false); await wrapped(change); expect(mockedJobIsEnabled).toBeCalled(); expect(mockedResume).toBeCalled(); expect(mockedPause).not.toBeCalled(); }); afterAll(() => { cleanup(); }); }); const updateStreamerStatus = async (streamerId: string, isOnline: boolean) => { const stremersRef = db.collection('streamers'); const query = stremersRef.where('broadcasterId', '==', streamerId); const querySnapshot = await query.get(); console.log('streamers', (await stremersRef.get()).docs.map((doc) => doc.data())); // 调试用日志 const streamerDocId = querySnapshot.docs[0].id; await stremersRef.doc(streamerDocId).update({ isOnline }); };
负责重置Firestore的工具函数代码:
import firebaseFunctionsTest from 'firebase-functions-test'; import { getApp, getApps, initializeApp, cert } from 'firebase-admin/app'; import { getFirestore } from 'firebase-admin/firestore'; const projectId = 'latamqchallengetest'; export const { wrap, cleanup, firestore: firestoreTestData, makeChange } = firebaseFunctionsTest({ databaseURL: `https://${projectId}.firebaseio.com`, storageBucket: `${projectId}.appspot.com`, projectId: projectId, }, './../google-credentials/latamqchallengetest-firebase-adminsdk.json'); export const initializeFirebase = () => { if (getApps().length <= 0) { initializeApp({ credential: cert('./../google-credentials/latamqchallengetest-firebase-adminsdk.json'), }); } return getApp(); }; export const getDb = () => { initializeFirebase(); const db = getFirestore(); return db; }; export const wipeDb = async () => { const db = getDb(); const collections = await db.listCollections(); const deletePromises = collections.map((collectionRef) => db.recursiveDelete(collectionRef)); Promise.all([...deletePromises]); }; export const seed = async () => { const db = getDb(); await db.collection('streamers').add({ broadcasterId: '64522496', broadcasterName: 'ITonyl', color: '1565C0', description: 'Sr. Truchita', isOnline: false, puuid: 'kRQyIDe5TfLhWtn8jXgo_4Zjlfg4rPypXWiPCXrkTMUsiQT3TYVCkVO6Au3QTdd4x-13CbluPA53dg', summonerId: '9bnJOmLXDjX-sgPjn4ZN1_-f6m4Ojd2OWlNBzrdH0Xk2xw', }); await db.collection('streamers').add({ broadcasterId: '176444069', broadcasterName: 'Onokenn', color: '424242', description: 'El LVP Player', isOnline: false, puuid: 'Gbfj8FyB6OZewfXgAwvLGpkayA6xyevFfEW7UZdrzA6saKVTyntP4HxhBQFd_EIGa_P1xC9eVdy8sQ', summonerId: 'pPWUsvk_67FuF7ky1waWDM_gho-3NYP4enWTtte6deR3CjxOGoCfoIjjNw', }); }; export const resetDb = async () => { await wipeDb(); await seed(); };
当前问题:Firestore中的记录数不稳定,有时是4条、有时是0条、有时是2条。明明在每个测试前都await了重置数据库的Promise,却还是出现这种状态不一致的情况,我希望每次测试前都能保证数据库处于一致的初始状态。
1. 修复wipeDb函数的异步问题
你的wipeDb函数中调用了Promise.all但没有添加await,这会导致函数在删除操作还未完成时就提前返回,后续的seed操作会在数据库未清空的情况下执行,造成数据叠加或缺失。
修改后的wipeDb:
export const wipeDb = async () => { const db = getDb(); const collections = await db.listCollections(); const deletePromises = collections.map((collectionRef) => db.recursiveDelete(collectionRef)); // 等待所有删除操作完成后再继续执行 await Promise.all(deletePromises); };
2. 强制测试串行执行
多个测试套件并行运行时,会同时操作同一个Firestore实例,导致状态互相干扰。在Jest配置中添加--runInBand参数,强制所有测试串行执行:
在package.json的test脚本中修改:
"scripts": { "test": "jest --runInBand" }
3. 关闭Firestore本地持久化缓存
在线模式下Firestore默认会启用本地持久化缓存,可能导致测试读取到旧数据。在初始化Firestore时关闭持久化:
修改getDb函数:
export const getDb = () => { initializeFirebase(); const db = getFirestore(); // 关闭本地持久化,确保每次读取都是从服务器获取最新数据 db.settings({ persistence: false }); return db; };
4. 验证重置流程的原子性
修复后的resetDb会先等待wipeDb完全清空数据库,再执行seed初始化数据,保证了重置流程的原子性。如果仍有问题,可以在resetDb中添加日志,确认每个步骤的执行顺序和完成状态,方便进一步排查。
内容的提问来源于stack exchange,提问作者AndresSp

