如何用 sinon.js 存根 https.request response.pipe 并承诺和可能的路径?
How to stub https.request response.pipe with sinon.js and promise and possible paths?
我目前正在做与
中描述的类似的事情
实际上我也在做同样的事情,但我的代码有点复杂,因为我有多个关于请求代码处理的路径(条件),并使用承诺而不是回调。此外,我使用 https 而不是请求模块。
我目前有以下代码无法开始工作:
//utils.js
/**
* Fetches the repository archive
*
* @param url The archive download url
* @param dest The temp directory path
* @param accessToken The access token for the repository (only available if private repository)
*/
exports.fetchArchive = function(url, dest, accessToken) {
let options = {
headers: {}
}
if (accessToken) {
options.headers = {'PRIVATE-TOKEN': accessToken}
}
return new Promise((resolve, reject) => {
https
.get(url, options,(response) => {
const code = response.statusCode;
if (code >= 400) {
reject({ code, message: response.statusMessage });
} else if (code >= 300) {
this.fetchArchive(response.headers.location, dest).then(resolve, reject);
} else {
response
.pipe(fs.createWriteStream(dest))
.on('end', () => resolve(null))
.on('error', () => reject({ code, message: response.statusMessage }));
}
})
});
}
_
//utils.test.js
describe('public fetchArchive', () => {
it(`should have a redirect status code (>= 300) and redirect and thus be called at twice`, () => {
let options = {
headers: {}
}
options.headers = {'PRIVATE-TOKEN': repoPropsPrivate.accessToken}
const mockResponse = `{"data": 123}`;
// //Using a built-in PassThrough stream to emit needed data.
const mockStream = new PassThrough();
mockStream.push(mockResponse);
mockStream.end(); //Mark that we pushed all the data.
sinon
.stub(https, 'get')
.callsFake(function (privateUrl, options, callback) {
callback(mockStream);
return Promise.resolve(null); //Stub end method btw
});
//Finally keep track of how 'pipe' is going to be called
sinon.spy(mockStream, 'pipe');
return utils.fetchArchive(privateArchiveUrl, privateArchiveDest, repoPropsPrivate.accessToken)
.then((res) => {
sinon.assert.calledOnce(mockStream.pipe);
//We can get the stream that we piped to.
let writable = mockStream.pipe.getCall(0).args[0];
assert.equal(writable.path, './output.json');
})
});
});
我不确定如何调整其他 post 的代码以满足我的要求。
我不知道如何发送包含请求代码和流的响应,以及如何在测试中处理承诺。
非常感谢您的帮助。
您可以创建自定义可写流以便更好地控制模拟数据,例如在流对象上添加 statusCode
或 headers
属性。我会创建这样的东西(这模拟了一个实际的流,其中 _read()
将从给定的 responseBody
发出 4 个字节的数据,直到没有任何内容可读:
const {Readable} = require('stream');
class ResponseStreamMock extends Readable {
constructor(statusCode, responseBody) {
super();
this.statusCode = statusCode;
this.headers = {location: 'someLocation'};
this.responseData = responseBody !== undefined ? Buffer.from(JSON.stringify(responseBody), "utf8") : Buffer.from([]);
this.bytesRead = 0;
this.offset = 4;
}
_read() {
if (this.bytesRead >= this.responseData.byteLength) {
this.push(null);
} else {
setTimeout(() => {
const buff = this.responseData.toString('utf8', this.bytesRead, this.bytesRead + this.offset);
this.push(Buffer.from(buff));
this.bytesRead += this.offset;
}, Math.random() * 200)
}
}
}
然后您可以在 unit-test:
中像这样使用它
describe('public fetchArchive', () => {
it(`should redirect for status code >= 300, then pipe response into fs-stream`, async function () {
this.timeout(5000)
const httpGetStub = sinon.stub(https, 'get');
let redirectStream = new ResponseStreamMock(300);
let responseStream = new ResponseStreamMock(200, {"someData": {"test": 123}});
httpGetStub.onCall(0).callsFake(function (privateUrl, options, callback) {
redirectStream.resume(); // immediately flush the stream as this one does not get piped
callback(redirectStream);
});
httpGetStub.onCall(1).callsFake(function (privateUrl, options, callback) {
callback(responseStream);
});
sinon.spy(redirectStream, 'pipe');
sinon.spy(responseStream, 'pipe');
const fsWriteStreamStub = sinon
.stub(fs, 'createWriteStream').callsFake(() => process.stdout); // you can replace process.stdout with new PassThrough() if you don't want any output
const result = await fetchArchive("someURL", "someDestination", "")
sinon.assert.calledOnce(fsWriteStreamStub);
sinon.assert.calledOnce(responseStream.pipe);
sinon.assert.notCalled(redirectStream.pipe);
assert.equal(result, null);
});
});
请注意,您需要稍微更改 fetchArchive
的实现才能使其正常工作:
exports.fetchArchive = function fetchArchive(url, dest, accessToken) {
let options = {
headers: {}
}
if (accessToken) {
options.headers = {'PRIVATE-TOKEN': accessToken}
}
return new Promise((resolve, reject) => {
https
.get(url, options, (response) => {
const code = response.statusCode;
if (code >= 400) {
reject({code, message: response.statusMessage});
} else if (code >= 300) {
fetchArchive(response.headers.location, dest).then(resolve, reject);
} else {
response.on('end', () => resolve(null))
response.on('error', () => reject({code, message: response.statusMessage}));
response.pipe(fs.createWriteStream(dest))
}
})
});
}
我目前正在做与
实际上我也在做同样的事情,但我的代码有点复杂,因为我有多个关于请求代码处理的路径(条件),并使用承诺而不是回调。此外,我使用 https 而不是请求模块。
我目前有以下代码无法开始工作:
//utils.js
/**
* Fetches the repository archive
*
* @param url The archive download url
* @param dest The temp directory path
* @param accessToken The access token for the repository (only available if private repository)
*/
exports.fetchArchive = function(url, dest, accessToken) {
let options = {
headers: {}
}
if (accessToken) {
options.headers = {'PRIVATE-TOKEN': accessToken}
}
return new Promise((resolve, reject) => {
https
.get(url, options,(response) => {
const code = response.statusCode;
if (code >= 400) {
reject({ code, message: response.statusMessage });
} else if (code >= 300) {
this.fetchArchive(response.headers.location, dest).then(resolve, reject);
} else {
response
.pipe(fs.createWriteStream(dest))
.on('end', () => resolve(null))
.on('error', () => reject({ code, message: response.statusMessage }));
}
})
});
}
_
//utils.test.js
describe('public fetchArchive', () => {
it(`should have a redirect status code (>= 300) and redirect and thus be called at twice`, () => {
let options = {
headers: {}
}
options.headers = {'PRIVATE-TOKEN': repoPropsPrivate.accessToken}
const mockResponse = `{"data": 123}`;
// //Using a built-in PassThrough stream to emit needed data.
const mockStream = new PassThrough();
mockStream.push(mockResponse);
mockStream.end(); //Mark that we pushed all the data.
sinon
.stub(https, 'get')
.callsFake(function (privateUrl, options, callback) {
callback(mockStream);
return Promise.resolve(null); //Stub end method btw
});
//Finally keep track of how 'pipe' is going to be called
sinon.spy(mockStream, 'pipe');
return utils.fetchArchive(privateArchiveUrl, privateArchiveDest, repoPropsPrivate.accessToken)
.then((res) => {
sinon.assert.calledOnce(mockStream.pipe);
//We can get the stream that we piped to.
let writable = mockStream.pipe.getCall(0).args[0];
assert.equal(writable.path, './output.json');
})
});
});
我不确定如何调整其他 post 的代码以满足我的要求。
我不知道如何发送包含请求代码和流的响应,以及如何在测试中处理承诺。
非常感谢您的帮助。
您可以创建自定义可写流以便更好地控制模拟数据,例如在流对象上添加 statusCode
或 headers
属性。我会创建这样的东西(这模拟了一个实际的流,其中 _read()
将从给定的 responseBody
发出 4 个字节的数据,直到没有任何内容可读:
const {Readable} = require('stream');
class ResponseStreamMock extends Readable {
constructor(statusCode, responseBody) {
super();
this.statusCode = statusCode;
this.headers = {location: 'someLocation'};
this.responseData = responseBody !== undefined ? Buffer.from(JSON.stringify(responseBody), "utf8") : Buffer.from([]);
this.bytesRead = 0;
this.offset = 4;
}
_read() {
if (this.bytesRead >= this.responseData.byteLength) {
this.push(null);
} else {
setTimeout(() => {
const buff = this.responseData.toString('utf8', this.bytesRead, this.bytesRead + this.offset);
this.push(Buffer.from(buff));
this.bytesRead += this.offset;
}, Math.random() * 200)
}
}
}
然后您可以在 unit-test:
中像这样使用它describe('public fetchArchive', () => {
it(`should redirect for status code >= 300, then pipe response into fs-stream`, async function () {
this.timeout(5000)
const httpGetStub = sinon.stub(https, 'get');
let redirectStream = new ResponseStreamMock(300);
let responseStream = new ResponseStreamMock(200, {"someData": {"test": 123}});
httpGetStub.onCall(0).callsFake(function (privateUrl, options, callback) {
redirectStream.resume(); // immediately flush the stream as this one does not get piped
callback(redirectStream);
});
httpGetStub.onCall(1).callsFake(function (privateUrl, options, callback) {
callback(responseStream);
});
sinon.spy(redirectStream, 'pipe');
sinon.spy(responseStream, 'pipe');
const fsWriteStreamStub = sinon
.stub(fs, 'createWriteStream').callsFake(() => process.stdout); // you can replace process.stdout with new PassThrough() if you don't want any output
const result = await fetchArchive("someURL", "someDestination", "")
sinon.assert.calledOnce(fsWriteStreamStub);
sinon.assert.calledOnce(responseStream.pipe);
sinon.assert.notCalled(redirectStream.pipe);
assert.equal(result, null);
});
});
请注意,您需要稍微更改 fetchArchive
的实现才能使其正常工作:
exports.fetchArchive = function fetchArchive(url, dest, accessToken) {
let options = {
headers: {}
}
if (accessToken) {
options.headers = {'PRIVATE-TOKEN': accessToken}
}
return new Promise((resolve, reject) => {
https
.get(url, options, (response) => {
const code = response.statusCode;
if (code >= 400) {
reject({code, message: response.statusMessage});
} else if (code >= 300) {
fetchArchive(response.headers.location, dest).then(resolve, reject);
} else {
response.on('end', () => resolve(null))
response.on('error', () => reject({code, message: response.statusMessage}));
response.pipe(fs.createWriteStream(dest))
}
})
});
}