单元测试触发Firebase Functions时数据异常问题求助
问题
在通过模拟器对Cloud Functions的onDocumentWritten触发器进行单元测试时,调用包装后的函数始终无法获取正确的输入数据——event.data.after.data()返回的总是默认占位符{ aString: 'foo', anObject: { a: 'qux', b: 'faz' }, aNumber: 7 },无论是传入Firestore写入操作的结果,还是用testEnv生成的Change对象,问题都存在。
测试文件代码
import 'jest'; import * as crypto from 'node:crypto' import * as admin from 'firebase-admin'; export function generateTempId() { return 'tmp-' + crypto.randomUUID() } process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080' const app = admin.initializeApp({ projectId: '<project-id>', credential: admin.credential.applicationDefault() }) const db = app.firestore() const testEnv = require("firebase-functions-test")({ databaseURL: 'https://<project-id>.firebaseio.com', storageBucket: '<project-id>.appspot.com', projectId: '<project-id>', }) const {myOnWriteTrigger} = require("../src/db.ts") describe("Item Editing", () => { let wrapped; beforeAll(() => { wrapped = testEnv.wrap(myOnWriteTrigger) }) test("Write Item", async () => { const path = `items/${generateTempId()}` const data = {name: "MyTest"} const s = await db.doc(path).set(data) const after = await wrapped(s) console.log(after.data()) }) })
触发器代码
export const myOnWriteTrigger = onDocumentWritten("items/{itemid}", async (event) => { // 此处始终打印默认占位符 console.log(event.data.after.data()) if (event.data?.after?.data() == null) { console.log("after is null") return } const afterData = event.data.after.data() if (afterData.name.includes("My")) { const newName = afterData.name.replace("My", "") await event.data.after.ref.update({name: newName}) } return })
尝试过的其他方式(仍无效)
const beforeSnap = testEnv.firestore.makeDocumentSnapshot({name:'blah'}, path) const afterSnap = testEnv.firestore.makeDocumentSnapshot(data, path) const change = testEnv.makeChange(beforeSnap, afterSnap) const after = await wrapped(change)
原因分析与修复方案
核心原因
firebase-functions-test默认开启自动模拟,会用占位符数据替换传入的真实快照,导致触发器无法获取自定义数据。admin应用初始化时机错误,先于testEnv初始化,导致测试环境的模拟无法覆盖原生Admin SDK的行为。- 直接传入Firestore
set操作的返回值(WriteResult)不符合包装函数要求的Change对象类型。
修复步骤
- 禁用自动模拟:初始化
testEnv时添加mockConfig: false参数,强制使用传入的真实数据。 - 调整初始化顺序:先初始化
testEnv,再初始化admin应用,确保测试环境的模拟优先生效。 - 正确构造输入参数:必须用
testEnv.makeChange生成符合要求的Change对象,不能直接传入WriteResult。
修复后的测试文件
import 'jest'; import * as crypto from 'node:crypto'; import * as admin from 'firebase-admin'; export function generateTempId() { return 'tmp-' + crypto.randomUUID(); } // 1. 先初始化testEnv,禁用自动模拟 const testEnv = require("firebase-functions-test")({ databaseURL: 'https://<project-id>.firebaseio.com', storageBucket: '<project-id>.appspot.com', projectId: '<project-id>', }, undefined, { mockConfig: false }); // 关键配置 // 2. 再初始化Admin SDK process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080'; const app = admin.initializeApp({ projectId: '<project-id>', credential: admin.credential.applicationDefault() }); const db = app.firestore(); const { myOnWriteTrigger } = require("../src/db.ts"); describe("Item Editing", () => { let wrapped; beforeAll(() => { wrapped = testEnv.wrap(myOnWriteTrigger); }); test("Write Item", async () => { const path = `items/${generateTempId()}`; const data = { name: "MyTest" }; // 3. 正确构造Change对象 const beforeSnap = testEnv.firestore.makeDocumentSnapshot({}, path); // 写入前空快照 const afterSnap = testEnv.firestore.makeDocumentSnapshot(data, path); // 写入后快照 const change = testEnv.makeChange(beforeSnap, afterSnap); await wrapped(change); // 验证触发器执行结果 const updatedDoc = await db.doc(path).get(); console.log(updatedDoc.data()); // 预期输出: { name: "Test" } }); // 测试后清理资源 afterAll(async () => { await testEnv.cleanup(); await app.delete(); }); });
额外注意事项
- 测试结束后调用
testEnv.cleanup()和app.delete(),避免残留资源影响后续测试。 - 若需验证Firestore模拟器中的数据变化,确保
FIRESTORE_EMULATOR_HOST环境变量已正确指向本地模拟器地址。
内容的提问来源于stack exchange,提问作者Michael Berk
相关产品推荐
相关产品推荐

