如何离线为Firebase(Firestore/Realtime Database/Storage)操作函数编写单元测试
离线状态下Firebase自定义函数单元测试方案
针对提出的约束(不使用firebase-functions-test、不使用已废弃的@firebase/testing),以下两种方案都可以实现完全离线的单元测试,覆盖Firestore、Realtime Database、Storage三类服务的操作,以提供的setDoc函数为例具体实现如下:
方案1:基于官方本地模拟器的行为一致性测试
这个方案依托Firebase官方提供的本地模拟器套件实现,所有服务运行在本地内存中,完全不连接云端资源,行为和线上Firebase服务100%一致,不需要额外引入废弃测试依赖。
- 前置依赖安装
仅需要安装三个类别的依赖:- 生产依赖:已有的
firebase-admin - 测试框架:任意JS测试框架,示例用Jest
- 工具: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
相关产品推荐
相关产品推荐

