如何用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
相关产品推荐
相关产品推荐

