You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修复Jest中Cloud Functions的Firestore单元测试状态不一致问题

问题: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 22:25:32