使用Vitest测试Firebase可调用函数遇TypeError错误,求解决
问题原因
functions.https.onCall()返回的是Firebase部署用的触发器处理函数,并非直接接收data和context的普通异步函数。你直接调用它时,内部逻辑会尝试处理Firebase的请求对象,而非你手动传入的简单对象,因此抛出res.on is not a function错误。
解决方案:拆分业务逻辑与触发器包装
将核心的元数据抓取逻辑抽离为纯函数,单独测试纯函数;Firebase可调用函数仅作为入口调用该纯函数,这样既保证业务逻辑可测试,又不影响生产环境的触发器功能。
1. 改造原函数文件(getMetadata.ts)
import * as functions from "firebase-functions"; import metascraper from "metascraper"; import metascraperTitle from "metascraper-title"; import metascraperDescription from "metascraper-description"; // 抽离核心业务逻辑为纯函数,仅负责元数据抓取,无Firebase依赖 export async function scrapeMetadata(url) { try { const res = await fetch(url); const html = await res.text(); const metadata = await metascraper([ metascraperTitle(), metascraperDescription(), ])({ html, url }); return { title: metadata.title, description: metadata.description, }; } catch (error) { functions.logger.error(error); throw error; } } // Firebase可调用函数入口,处理参数校验和错误转换 export const getMetadata = functions.https.onCall(async (data, context) => { try { if (!data.url) { throw new functions.https.HttpsError("invalid-argument", "URL参数不能为空"); } return await scrapeMetadata(data.url); } catch (error) { functions.logger.error(error); // 统一转换为Firebase客户端可识别的错误格式 if (error instanceof functions.https.HttpsError) { throw error; } throw new functions.https.HttpsError("internal", "元数据抓取失败"); } });
2. 修改测试文件(getMetadata.test.ts)
使用Vitest的Mock功能模拟fetch,避免实际请求外部网站,专注测试业务逻辑:
import { describe, it, expect, vi } from "vitest"; import { scrapeMetadata } from "./getMetadata"; describe('scrapeMetadata', () => { it('应返回指定URL的元数据', async () => { // 模拟fetch返回example.com的HTML内容 vi.spyOn(global, 'fetch').mockResolvedValue({ text: async () => ` <!doctype html> <html> <head> <title>Example Domain</title> <meta charset="utf-8" /> <meta http-equiv="Content-type" content="text/html; charset=utf-8" /> </head> <body></body> </html> `, } as Response); const result = await scrapeMetadata('https://example.com'); expect(result).toEqual({ title: 'Example Domain', description: null }); expect(fetch).toHaveBeenCalledWith('https://example.com'); // 清理Mock vi.restoreAllMocks(); }); it('fetch失败时应抛出错误', async () => { vi.spyOn(global, 'fetch').mockRejectedValue(new Error('请求失败')); await expect(scrapeMetadata('https://invalid.example')).rejects.toThrow('请求失败'); vi.restoreAllMocks(); }); });
额外说明
如果需要测试完整的Firebase可调用函数(比如参数校验、错误返回格式),可以使用firebase-functions-test库模拟Firebase运行环境,但测试纯函数的方式更高效、更聚焦业务逻辑。
内容的提问来源于stack exchange,提问作者Julien Reszka
相关产品推荐
相关产品推荐

