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

如何用Jest为OpenAI Node SDK(streaming=true)编写集成测试

问题:测试OpenAI Node.js SDK流式响应时的拦截与模拟问题

我在给使用streaming: true的OpenAI Node.js SDK写测试时遇到麻烦,浏览器里代码跑正常,但测试里拦不住OpenAI的请求,也没法通过模拟返回正确响应让代码继续执行。我想测试整个Express路由,不限制用现有Mock库,求替代方案。

测试代码

it("should return stream the ai output if this is the first time fetching the document", async () => {
      nock("https://api.openai.com")
        .post("/v1/chat/completions")
        .reply(200, {
          id: "chatcmpl-123",
          object: "chat.completion.chunk",
          created: 1694268190,
          model: "gpt-3.5-turbo-0613",
          system_fingerprint: "fp_44709d6fcb",
          choices: [
            {
              index: 0,
              delta: {
                role: "assistant",
                content: "I am actually not OpenAI.",
              },
            },
          ],
          finish_reason: "stop",
        });
      const response = await agent.get(`${baseRoute}/documents/${documentId}`);
      expect(response.status).toBe(200);
      console.log(response.body);
    });

/api/v2/documents/:id控制器代码

const stream = await openaiService.getCompletionStream(document);
response.setHeader("Transfer-Encoding", "chunked");
response.setHeader("X-Content-Type-Options", "nosniff");
for await (const chunk of stream) {
  const data = chunk.choices[0]?.delta?.content;
  if (data !== undefined) {
    response.write(data);
  }
}

response.end();

被调用的getCompletionStream函数代码

getCompletionStream = async (document: DocumentDtoV2) => {
    const openai = this.getOpenAiAgent();
    const completion = await openai.chat.completions.create({
      model: document.aiEngine.defaultProperties.model,
      messages: document.messages,
      max_tokens: document.aiEngine.defaultProperties.max_tokens,
      stream: true,
      temperature: document.prompt.model.customProperties.temperature
        ? document.prompt.model.customProperties.temperature
        : document.aiEngine.defaultProperties.temperature,
      top_p: document.prompt.model.customProperties.top_p
        ? document.prompt.model.customProperties.top_p
        : document.aiEngine.defaultProperties.top_p,
      frequency_penalty: (document.aiEngine.defaultProperties as ChatGPTProperties)
        .frequency_penalty,
      presence_penalty: (document.aiEngine.defaultProperties as ChatGPTProperties).presence_penalty,
    });

    return completion;
  };

解决方案

核心问题分析

nock默认返回完整JSON响应,但OpenAI流式接口返回的是分块的Server-Sent Events(SSE),格式为data: {JSON chunk}\n\n,直接返回单个JSON对象模拟不了流式响应,导致代码里的for await...of循环无法正确处理。

方案1:修复nock的流式响应模拟

修改nock的reply,模拟SSE格式的分块响应:

it("should return stream the ai output if this is the first time fetching the document", async () => {
  nock("https://api.openai.com")
    .post("/v1/chat/completions")
    .reply(200, () => {
      // 模拟SSE格式的多块数据
      return [
        `data: ${JSON.stringify({
          id: "chatcmpl-123",
          object: "chat.completion.chunk",
          created: 1694268190,
          model: "gpt-3.5-turbo-0613",
          choices: [{ index: 0, delta: { role: "assistant", content: "I am" } }]
        })}\n\n`,
        `data: ${JSON.stringify({
          id: "chatcmpl-123",
          object: "chat.completion.chunk",
          created: 1694268190,
          model: "gpt-3.5-turbo-0613",
          choices: [{ index: 0, delta: { content: " actually not OpenAI." } }]
        })}\n\n`,
        `data: [DONE]\n\n`
      ].join('');
    }, {
      'Content-Type': 'text/event-stream',
      'Transfer-Encoding': 'chunked'
    });

  const response = await agent.get(`${baseRoute}/documents/${documentId}`);
  expect(response.status).toBe(200);
  expect(response.text).toBe("I am actually not OpenAI.");
});

方案2:直接Mock OpenAI SDK(更可靠)

绕过HTTP拦截,直接mock openai.chat.completions.create方法,返回模拟的异步迭代器:

// Mock openaiService模块
jest.mock('../path/to/openaiService', () => {
  // 模拟流式响应的异步生成器
  const mockStream = async function*() {
    yield { choices: [{ delta: { role: "assistant", content: "I am" } }] };
    yield { choices: [{ delta: { content: " actually not OpenAI." } }] };
  };

  return {
    getCompletionStream: jest.fn().mockReturnValue(mockStream())
  };
});

it("should return stream the ai output if this is the first time fetching the document", async () => {
  const response = await agent.get(`${baseRoute}/documents/${documentId}`);
  expect(response.status).toBe(200);
  expect(response.text).toBe("I am actually not OpenAI.");
  // 验证方法被调用
  expect(require('../path/to/openaiService').getCompletionStream).toHaveBeenCalled();
});

方案3:监听流式响应的分块传输

如果需要验证每一块的传输过程,用supertest的on('data')事件监听:

it("should stream ai output chunks", (done) => {
  nock("https://api.openai.com")
    .post("/v1/chat/completions")
    .reply(200, function() {
      const chunks = [
        `data: ${JSON.stringify({ choices: [{ delta: { content: "I am" } }] })}\n\n`,
        `data: ${JSON.stringify({ choices: [{ delta: { content: " actually not" } }] })}\n\n`,
        `data: ${JSON.stringify({ choices: [{ delta: { content: " OpenAI." } }] })}\n\n`,
        `data: [DONE]\n\n`
      ];
      chunks.forEach(chunk => this.response.write(chunk));
      this.response.end();
    }, { 'Content-Type': 'text/event-stream' });

  agent.get(`${baseRoute}/documents/${documentId}`)
    .expect(200)
    .on('data', (chunk) => {
      // 验证每一块内容
      expect(chunk.toString()).toMatch(/I am|actually not|OpenAI/);
    })
    .on('end', done);
});

内容的提问来源于stack exchange,提问作者user16197389

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 00:02:43