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

如何用Sinon.js结合Promise模拟https.get的response.pipe及多分支场景?

解决Sinon模拟https.get响应及Promise/pipe测试的问题

你遇到的核心问题在于没有正确模拟HTTP响应对象的完整属性(比如状态码、响应头),以及未处理递归重定向的多轮调用逻辑,还有对https.get返回值的误解。下面一步步拆解解决方案:


1. 模拟完整的HTTP响应对象

https.get的回调参数是一个IncomingMessage对象,它不仅是可读流,还包含statusCode、statusMessage、headers等关键属性。你需要给你的PassThrough流添加这些属性,才能触发业务代码里的状态码分支逻辑:

const { PassThrough } = require('stream');

// 生成模拟响应的工具函数
function createMockResponse(statusCode, headers = {}, streamData = '') {
  const mockRes = new PassThrough();
  // 添加HTTP响应核心属性
  mockRes.statusCode = statusCode;
  mockRes.statusMessage = statusCode === 200 ? 'OK' : 'Found';
  mockRes.headers = headers;
  // 写入流数据
  mockRes.push(streamData);
  mockRes.end();
  return mockRes;
}

2. 处理递归重定向的Stub逻辑

要测试重定向场景(状态码≥300),需要让https.get的stub第一次返回重定向响应,第二次返回成功响应,这样才能触发fetchArchive的递归调用。可以用callsFake结合调用次数来实现:

describe('public fetchArchive', () => {
  let getStub;

  beforeEach(() => {
    // 初始化stub
    getStub = sinon.stub(https, 'get');
  });

  afterEach(() => {
    // 恢复原始方法,避免影响其他测试
    getStub.restore();
  });

  it('should handle redirects and call fetchArchive twice', async () => {
    const privateArchiveUrl = 'https://example.com/archive';
    const redirectUrl = 'https://example.com/redirect-archive';
    const privateArchiveDest = './output.json';
    const repoPropsPrivate = { accessToken: 'test-token' };

    // 第一次调用:返回302重定向响应
    getStub.onFirstCall().callsFake((url, options, callback) => {
      callback(createMockResponse(302, { location: redirectUrl }));
      // https.get返回的是ClientRequest对象,不是Promise,模拟空对象即可
      return { on: () => {} };
    });

    // 第二次调用:返回200成功响应,带模拟归档数据
    getStub.onSecondCall().callsFake((url, options, callback) => {
      const mockRes = createMockResponse(200, {}, 'mock-archive-content');
      sinon.spy(mockRes, 'pipe'); // 监听pipe方法调用
      callback(mockRes);
      return { on: () => {} };
    });

    // 执行测试方法,用async/await处理Promise
    await utils.fetchArchive(privateArchiveUrl, privateArchiveDest, repoPropsPrivate.accessToken);

    // 验证https.get被调用两次,且URL符合预期
    sinon.assert.calledTwice(getStub);
    sinon.assert.calledWith(getStub.firstCall, privateArchiveUrl, sinon.match.any, sinon.match.func);
    sinon.assert.calledWith(getStub.secondCall, redirectUrl, sinon.match.any, sinon.match.func);

    // 验证pipe方法被调用,且写入流路径正确
    const secondResponse = getStub.secondCall.args[2].args[0];
    sinon.assert.calledOnce(secondResponse.pipe);
    const writableStream = secondResponse.pipe.getCall(0).args[0];
    assert.equal(writableStream.path, privateArchiveDest);
  });
});

3. 修正业务代码的潜在问题

你的fetchArchive里有一个this指向的坑:在Promise回调里this的指向不是exports对象,递归调用会失败,建议改成直接引用exports.fetchArchive:

// 原代码
this.fetchArchive(response.headers.location, dest).then(resolve, reject);

// 修改为(带上accessToken参数,私有仓库重定向后仍需令牌)
exports.fetchArchive(response.headers.location, dest, accessToken).then(resolve, reject);

4. 错误场景的测试示例(可选)

如果需要测试4xx错误的情况,同样用工具函数生成对应状态码的响应:

it('should reject with error when status code is 4xx', async () => {
  getStub.callsFake((url, options, callback) => {
    callback(createMockResponse(404, {}, ''));
    return { on: () => {} };
  });

  await expect(utils.fetchArchive('https://example.com/404', './dest'))
    .rejects.toMatchObject({ code: 404, message: 'Not Found' });
});

关键要点总结

  • 模拟IncomingMessage时,必须给流添加statusCode、headers等属性,否则业务代码的分支逻辑不会触发
  • https.get返回的是ClientRequest对象,不是Promise,不要错误地返回Promise
  • 测试递归/多轮调用时,用onFirstCall/onSecondCall来区分不同调用的响应
  • 用async/await处理Promise,让测试代码更简洁易读

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 20:18:12