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

单元测试触发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)

原因分析与修复方案

核心原因

  1. firebase-functions-test默认开启自动模拟,会用占位符数据替换传入的真实快照,导致触发器无法获取自定义数据。
  2. admin应用初始化时机错误,先于testEnv初始化,导致测试环境的模拟无法覆盖原生Admin SDK的行为。
  3. 直接传入Firestore set操作的返回值(WriteResult)不符合包装函数要求的Change对象类型。

修复步骤

  1. 禁用自动模拟:初始化testEnv时添加mockConfig: false参数,强制使用传入的真实数据。
  2. 调整初始化顺序:先初始化testEnv,再初始化admin应用,确保测试环境的模拟优先生效。
  3. 正确构造输入参数:必须用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 11:40:10