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

如何离线为Firebase(Firestore/Realtime Database/Storage)操作函数编写单元测试

离线状态下Firebase自定义函数单元测试方案

针对提出的约束(不使用firebase-functions-test、不使用已废弃的@firebase/testing),以下两种方案都可以实现完全离线的单元测试,覆盖Firestore、Realtime Database、Storage三类服务的操作,以提供的setDoc函数为例具体实现如下:


方案1:基于官方本地模拟器的行为一致性测试

这个方案依托Firebase官方提供的本地模拟器套件实现,所有服务运行在本地内存中,完全不连接云端资源,行为和线上Firebase服务100%一致,不需要额外引入废弃测试依赖。

  • 前置依赖安装
    仅需要安装三个类别的依赖:
    1. 生产依赖:已有的firebase-admin
    2. 测试框架:任意JS测试框架,示例用Jest
    3. 工具:Firebase CLI(用于启动本地模拟器,首次安装下载对应服务镜像后即可完全离线运行,不需要登录账号)
  • 测试初始化配置
    在测试入口文件配置环境变量,让admin SDK指向本地模拟器地址,不需要提供真实服务账号密钥:
// 测试全局配置文件(如jest.setup.js)
const admin = require('firebase-admin');

// 配置三类服务的本地模拟器地址,按需开启
process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080';
process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9000';
process.env.FIREBASE_STORAGE_EMULATOR_HOST = 'localhost:9199';

// 初始化admin实例,项目ID随便填写即可,不需要真实存在
if (!admin.apps.length) {
  admin.initializeApp({ projectId: 'test-local-project' });
}
  • 编写测试用例
    每个用例执行完成后清空测试数据,避免用例间互相干扰:
const admin = require('firebase-admin');
const { setDoc } = require('./your-module-path');

describe('setDoc 函数测试', () => {
  afterEach(async () => {
    // 批量清空所有测试数据
    const collections = await admin.firestore().listCollections();
    await Promise.all(collections.map(col => col.recursiveDelete()));
  });

  test('默认merge=true时写入不会覆盖原有字段', async () => {
    const testPath = 'test_collection/test_doc';
    const db = admin.firestore();
    // 预置测试数据
    await db.doc(testPath).set({ old_field: 'old_val' });
    // 执行待测试函数
    await setDoc(testPath, { new_field: 'new_val' });
    // 校验结果
    const snap = await db.doc(testPath).get();
    expect(snap.data()).toEqual({
      old_field: 'old_val',
      new_field: 'new_val'
    });
  });

  test('传入merge=false时写入会覆盖全量字段', async () => {
    const testPath = 'test_collection/test_doc2';
    const db = admin.firestore();
    await db.doc(testPath).set({ old_field: 'old_val' });
    await setDoc(testPath, { new_field: 'new_val' }, false);
    const snap = await db.doc(testPath).get();
    expect(snap.data()).toEqual({ new_field: 'new_val' });
  });
});
  • 配置测试运行命令
    在package.json中配置脚本,启动模拟器后自动执行测试,测试完成后自动关闭模拟器进程:
{
  "scripts": {
    "test": "firebase emulators:exec --only firestore,database,storage --no-ui 'jest'"
  }
}

该方案优势是不需要自己实现模拟逻辑,所有Firebase的原生行为(包括数据类型校验、查询规则、事务逻辑、Storage上传下载规则等)都和线上完全一致,适合对逻辑准确性要求高的测试场景。


方案2:纯Mock实现轻量极速单测

如果不想启动模拟器进程,追求毫秒级的测试运行速度,可以直接通过模块Mock的方式模拟firebase-admin的所有行为,整个测试过程零外部依赖、完全离线。

  • 测试实现代码
    直接在测试文件中Mockfirebase-admin模块,用内存变量记录调用参数和数据,不需要修改业务代码:
// setDoc.test.js
// 定义Mock逻辑,必须在导入待测试函数前声明
const mockDocSet = jest.fn();
const mockDoc = jest.fn(() => ({ set: mockDocSet }));
const mockFirestore = jest.fn(() => ({ doc: mockDoc }));
jest.mock('firebase-admin', () => ({
  firestore: mockFirestore,
  apps: [],
  initializeApp: jest.fn(),
  // 若要测试Realtime Database/Storage,在这里对应mock database()、storage()方法即可
}));

// 导入待测试函数
const { setDoc } = require('./your-module-path');

describe('setDoc 纯Mock测试', () => {
  beforeEach(() => {
    // 每个用例执行前清空Mock调用记录
    jest.clearAllMocks();
  });

  test('默认调用时透传merge: true参数', async () => {
    const testPath = 'coll/doc';
    const testData = { key: 'value' };
    await setDoc(testPath, testData);
    // 校验路径参数正确
    expect(mockDoc).toHaveBeenCalledWith(testPath);
    // 校验set调用参数正确
    expect(mockDocSet).toHaveBeenCalledWith(testData, { merge: true });
  });

  test('传入merge=false时正确透传参数', async () => {
    const testPath = 'coll/doc2';
    const testData = { key2: 'value2' };
    await setDoc(testPath, testData, false);
    expect(mockDocSet).toHaveBeenCalledWith(testData, { merge: false });
  });
});

该方案优势是运行速度极快,不需要任何外部环境支持,缺点是复杂逻辑(如数据查询、字段合并结果、Storage文件校验等)需要自己实现对应Mock逻辑,适合校验参数传递正确性的轻量单测场景。


内容的提问来源于stack exchange,提问作者Mohammed Ali

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 05:45:08