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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 16:27:39