Nodejs - 使用 Mocha 测试 AWS
Nodejs - testing AWS with Mocha
我在为以下使用 AWS
和 graphicsmagick
的 nodejs
代码编写测试时遇到问题。我也尝试搜索有关如何为 async
的 waterfall
方法编写测试的示例,但没有任何明确的结果。
// dependencies
var async = require('async');
var AWS = require('aws-sdk');
var gm = require('gm').subClass({ imageMagick: true });
var util = require('util');
// get reference to S3 client
var s3 = new AWS.S3();
exports.AwsHandler = function(event, context) {
// Read options from the event.
console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
var srcBucket = event.Records[0].s3.bucket.name;
var srcKey = event.Records[0].s3.object.key;
var dstnKey = srcKey;
// Infer the image type.
var typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
console.error('unable to infer image type for key ' + srcKey);
return;
}
var imageType = typeMatch[1];
if (imageType != "jpg" && imageType != "png") {
console.log('skipping non-image ' + srcKey);
return;
}
//Download the image from S3, transform, and upload to same S3 bucket but different folders.
async.waterfall([
function download(next) {
// Download the image from S3 into a buffer.
s3.getObject({
Bucket: srcBucket,
Key: srcKey
},
next);
},
function transformSave(response, next) {
var _buffer = null;
for (var i = 0; i<len; i++) {
// Transform the image buffer in memory.
gm(response.Body, srcKey)
.resize(_sizesArray[i].width)
.toBuffer(imageType, function(err, buffer) {
if (err) {
next(err);
} else {
console.log(buffer);
_buffer = buffer;
}
});
// put newly resized image into respective folder
s3.putObject({
Bucket: srcBucket,
Key: "dst/" + _sizesArray[i].destinationPath + "/" + dstnKey,
Body: _buffer,
ContentType: response.ContentType
}, next);
}
},
], function (err) {
if (err) {
console.error(
'---->Unable to resize ' + srcBucket + '/' + srcKey +
' and upload to ' + srcBucket + '/dst' +
' due to an error: ' + err
);
} else {
console.log(
'---->Successfully resized ' + srcBucket +
' and uploaded to ' + srcBucket + "/dst"
);
}
context.done();
}
);
};
到目前为止我对该模块的测试:
require('blanket')({
pattern: function (filename) {
return !/node_modules/.test(filename);
}
});
// in terminal, type the following command to get code coverage: mocha -R html-cov > coverage.html
var chai = require('chai');
var sinonChai = require("sinon-chai");
var expect = chai.expect;
var sinon = require('sinon');
chai.use(sinonChai);
var sync = require("async");
var proxyquire = require('proxyquire');
describe('Image Resizing module', function () {
var gmSubclassStub = sinon.stub();
var getObjectStub = sinon.stub();
var putObjectSpy = sinon.spy();
var testedModule = proxyquire('../index', {
'gm': {subClass: sinon.stub().returns(gmSubclassStub)},
'AWS': {
"s3": {
getObject: sinon.stub().returns(getObjectStub),
putObject: putObjectSpy
}
}
});
describe('AwsHandler', function () {
var event = {
"Records": [
{
"s3": {
"bucket": {
"name": "testbucket"
},
"object": {
"key": "test.jpg"
}
}
}
]
};
it("should call gm write with correct files", function () {
// Arrange
// Spies are the methods you expect were actually called
var buffer800Spy = sinon.spy();
var buffer500Spy = sinon.spy();
var buffer200Spy = sinon.spy();
var buffer45Spy = sinon.spy();
// This is a stub that will return the correct spy for each iteration of the for loop
var resizeStub = sinon.stub();
resizeStub.withArgs(800).returns({toBuffer: buffer800Spy});
resizeStub.withArgs(500).returns({toBuffer: buffer500Spy});
resizeStub.withArgs(200).returns({toBuffer: buffer200Spy});
resizeStub.withArgs(45).returns({toBuffer: buffer45Spy});
// Stub is used when you just want to simulate a returned value
var nameStub = sinon.stub().yields({"name": "testbucket"});
var keyStub = sinon.stub().yields({"key": "test.jpg"});
gmSubclassStub.withArgs(event).returns({resize:resizeStub});
getObjectStub.withArgs(event).yields({name: nameStub}, {key: keyStub});
// Act - this calls the tested method
testedModule.AwsHandler(event);
// Assert
});
});
});
有几点需要修改:
您想测试单元的运行,而不测试执行。这就是为什么您应该忽略测试中的异步(就像您所做的那样)。
它只是实现方法的一种方式,即单元的内部工作原理。
您应该测试的是,在给定条件下,该单元给出预期的最终结果,在本例中它调用 s3.putObject。
所以你应该 stub 所有外部的东西(gm 和 aws),spy s3.putObject 方法,因为这是预期的最终结果。
在您的存根中,您使用了 "yield",它调用回调函数,但前提是它是第一个参数。
如果不是,就像在我们的例子中,您需要使用 "callsArgWith(index,...)" 和回调参数的索引。
proxyquire 的注入模块必须与需要它们的代码中的名称完全相同 - 将 'AWS' 更改为 'aws-sdk'
检查存根是否正确注入的一种方法是在调试器中,看一下
"s3" 变量,并检查它是 "function proxy()" 而不是 "function()"。如果你没有使用调试器,你也可以将它打印到控制台。
您的模块正在 for 循环中调用 next,这会导致瀑布分成一棵树,其中有 36 次调用 done(!)。
也许你应该使用不同的异步模型,比如 map reduce。我通过添加一个愚蠢的条件来修复它,但这不是好的代码。
附带说明一下,您可以看到测试变得非常复杂。
这可能表明被测试的代码可以使用某种关注点分离。
例如,将 gm 操作和 s3 操作移动到两个单独的模块中可以帮助分离事物,也更容易测试。
模块本身的变化,以防止调用 next 4*4 次:
function transform(response, next) {
for (var i = 0; i<len; i++) {
// Transform the image buffer in memory.
gm(response.Body, srcKey)
.resize(_sizesArray[i].width)
.toBuffer(imageType, function(err, buffer) {
if (err) {
next(err);
} else {
next(null, response.ContentType, buffer, i);
}
});
}
},
function upload(contentType, data, i, next) {
// Stream the transformed image to a different folder.
s3.putObject({
Bucket: srcBucket,
Key: "dst/" + _sizesArray[i].destinationPath + "/" + dstnKey,
Body: data,
ContentType: contentType
},
function(err) {
if (i==3) next(err);
});
}
测试:
describe.only('Image Resizing module', function () {
var gmSubclassStub = sinon.stub();
var s3Stub = {};
var proxyquire = require('proxyquire');
var testedModule = proxyquire('../index', {
'gm': {subClass: sinon.stub().returns(gmSubclassStub)},
'aws-sdk': {"S3": sinon.stub().returns(s3Stub)}
});
describe('AwsHandler', function () {
var event = {};
// The done callback is used for async testing
it("should call gm write with correct files", function (done) {
// Arrange
var resizeStub = sinon.stub();
var buffer800Spy = sinon.stub().withArgs("jpg").callsArgWith(1, null, "800 buffer");
var buffer500Spy = sinon.stub().withArgs("jpg").callsArgWith(1, null, "500 buffer");
var buffer200Spy = sinon.stub().withArgs("jpg").callsArgWith(1, null, "200 buffer");
var buffer45Spy = sinon.stub().withArgs("jpg").callsArgWith(1, null, "45 buffer");
resizeStub.withArgs(800).returns({toBuffer: buffer800Spy});
resizeStub.withArgs(500).returns({toBuffer: buffer500Spy});
resizeStub.withArgs(200).returns({toBuffer: buffer200Spy});
resizeStub.withArgs(45).returns({toBuffer: buffer45Spy});
gmSubclassStub.withArgs("response body", "test.jpg").returns({resize: resizeStub});
s3Stub.getObject = sinon.stub()
.withArgs({name: "testbucket", key: "test.jpg"})
.callsArgWith(1, null, {
Body: "response body",
ContentType: "response content type"
});
var putObjectMock = sinon.mock();
s3Stub.putObject = putObjectMock;
putObjectMock.callsArgWith(1, null, {}); // return behaviour of the mock
putObjectMock.exactly(4); // sets expectation that it is called 4 times
// Act - this calls the tested method
testedModule.AwsHandler(event, {
done: function () {
// Assertions need to be inside callback because it is async
assert.deepEqual(putObjectMock.getCall(0).args[0], {
Bucket: "testbucket",
Key: "dst/large/test.jpg",
Body: "800 buffer",
ContentType: "response content type"
});
assert.deepEqual(putObjectMock.getCall(1).args[0], {
Bucket: "testbucket",
Key: "dst/medium/test.jpg",
Body: "500 buffer",
ContentType: "response content type"
});
assert.deepEqual(putObjectMock.getCall(2).args[0], {
Bucket: "testbucket",
Key: "dst/small/test.jpg",
Body: "200 buffer",
ContentType: "response content type"
});
assert.deepEqual(putObjectMock.getCall(3).args[0], {
Bucket: "testbucket",
Key: "dst/thumbnail/test.jpg",
Body: "45 buffer",
ContentType: "response content type"
});
// This ends the async test
done();
}
});
});
});
});
这种问题很难在这里回答;这个问题不是很具体,也不是一个可以用意见、想法等来回答的开放式问题
因此,我创建了一个解决 async.waterfall
问题的类似实现,并提供了一个测试 AwsHandler
覆盖率达到 100% 的测试。
代码在这里 gist,因为它比这里更方便和可读。
我也写了一个blog post与这个实现相关的
我在为以下使用 AWS
和 graphicsmagick
的 nodejs
代码编写测试时遇到问题。我也尝试搜索有关如何为 async
的 waterfall
方法编写测试的示例,但没有任何明确的结果。
// dependencies
var async = require('async');
var AWS = require('aws-sdk');
var gm = require('gm').subClass({ imageMagick: true });
var util = require('util');
// get reference to S3 client
var s3 = new AWS.S3();
exports.AwsHandler = function(event, context) {
// Read options from the event.
console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
var srcBucket = event.Records[0].s3.bucket.name;
var srcKey = event.Records[0].s3.object.key;
var dstnKey = srcKey;
// Infer the image type.
var typeMatch = srcKey.match(/\.([^.]*)$/);
if (!typeMatch) {
console.error('unable to infer image type for key ' + srcKey);
return;
}
var imageType = typeMatch[1];
if (imageType != "jpg" && imageType != "png") {
console.log('skipping non-image ' + srcKey);
return;
}
//Download the image from S3, transform, and upload to same S3 bucket but different folders.
async.waterfall([
function download(next) {
// Download the image from S3 into a buffer.
s3.getObject({
Bucket: srcBucket,
Key: srcKey
},
next);
},
function transformSave(response, next) {
var _buffer = null;
for (var i = 0; i<len; i++) {
// Transform the image buffer in memory.
gm(response.Body, srcKey)
.resize(_sizesArray[i].width)
.toBuffer(imageType, function(err, buffer) {
if (err) {
next(err);
} else {
console.log(buffer);
_buffer = buffer;
}
});
// put newly resized image into respective folder
s3.putObject({
Bucket: srcBucket,
Key: "dst/" + _sizesArray[i].destinationPath + "/" + dstnKey,
Body: _buffer,
ContentType: response.ContentType
}, next);
}
},
], function (err) {
if (err) {
console.error(
'---->Unable to resize ' + srcBucket + '/' + srcKey +
' and upload to ' + srcBucket + '/dst' +
' due to an error: ' + err
);
} else {
console.log(
'---->Successfully resized ' + srcBucket +
' and uploaded to ' + srcBucket + "/dst"
);
}
context.done();
}
);
}; 到目前为止我对该模块的测试:
require('blanket')({
pattern: function (filename) {
return !/node_modules/.test(filename);
}
});
// in terminal, type the following command to get code coverage: mocha -R html-cov > coverage.html
var chai = require('chai');
var sinonChai = require("sinon-chai");
var expect = chai.expect;
var sinon = require('sinon');
chai.use(sinonChai);
var sync = require("async");
var proxyquire = require('proxyquire');
describe('Image Resizing module', function () {
var gmSubclassStub = sinon.stub();
var getObjectStub = sinon.stub();
var putObjectSpy = sinon.spy();
var testedModule = proxyquire('../index', {
'gm': {subClass: sinon.stub().returns(gmSubclassStub)},
'AWS': {
"s3": {
getObject: sinon.stub().returns(getObjectStub),
putObject: putObjectSpy
}
}
});
describe('AwsHandler', function () {
var event = {
"Records": [
{
"s3": {
"bucket": {
"name": "testbucket"
},
"object": {
"key": "test.jpg"
}
}
}
]
};
it("should call gm write with correct files", function () {
// Arrange
// Spies are the methods you expect were actually called
var buffer800Spy = sinon.spy();
var buffer500Spy = sinon.spy();
var buffer200Spy = sinon.spy();
var buffer45Spy = sinon.spy();
// This is a stub that will return the correct spy for each iteration of the for loop
var resizeStub = sinon.stub();
resizeStub.withArgs(800).returns({toBuffer: buffer800Spy});
resizeStub.withArgs(500).returns({toBuffer: buffer500Spy});
resizeStub.withArgs(200).returns({toBuffer: buffer200Spy});
resizeStub.withArgs(45).returns({toBuffer: buffer45Spy});
// Stub is used when you just want to simulate a returned value
var nameStub = sinon.stub().yields({"name": "testbucket"});
var keyStub = sinon.stub().yields({"key": "test.jpg"});
gmSubclassStub.withArgs(event).returns({resize:resizeStub});
getObjectStub.withArgs(event).yields({name: nameStub}, {key: keyStub});
// Act - this calls the tested method
testedModule.AwsHandler(event);
// Assert
});
});
});
有几点需要修改:
您想测试单元的运行,而不测试执行。这就是为什么您应该忽略测试中的异步(就像您所做的那样)。 它只是实现方法的一种方式,即单元的内部工作原理。 您应该测试的是,在给定条件下,该单元给出预期的最终结果,在本例中它调用 s3.putObject。 所以你应该 stub 所有外部的东西(gm 和 aws),spy s3.putObject 方法,因为这是预期的最终结果。
在您的存根中,您使用了 "yield",它调用回调函数,但前提是它是第一个参数。 如果不是,就像在我们的例子中,您需要使用 "callsArgWith(index,...)" 和回调参数的索引。
proxyquire 的注入模块必须与需要它们的代码中的名称完全相同 - 将 'AWS' 更改为 'aws-sdk' 检查存根是否正确注入的一种方法是在调试器中,看一下 "s3" 变量,并检查它是 "function proxy()" 而不是 "function()"。如果你没有使用调试器,你也可以将它打印到控制台。
您的模块正在 for 循环中调用 next,这会导致瀑布分成一棵树,其中有 36 次调用 done(!)。 也许你应该使用不同的异步模型,比如 map reduce。我通过添加一个愚蠢的条件来修复它,但这不是好的代码。
附带说明一下,您可以看到测试变得非常复杂。 这可能表明被测试的代码可以使用某种关注点分离。 例如,将 gm 操作和 s3 操作移动到两个单独的模块中可以帮助分离事物,也更容易测试。
模块本身的变化,以防止调用 next 4*4 次:
function transform(response, next) {
for (var i = 0; i<len; i++) {
// Transform the image buffer in memory.
gm(response.Body, srcKey)
.resize(_sizesArray[i].width)
.toBuffer(imageType, function(err, buffer) {
if (err) {
next(err);
} else {
next(null, response.ContentType, buffer, i);
}
});
}
},
function upload(contentType, data, i, next) {
// Stream the transformed image to a different folder.
s3.putObject({
Bucket: srcBucket,
Key: "dst/" + _sizesArray[i].destinationPath + "/" + dstnKey,
Body: data,
ContentType: contentType
},
function(err) {
if (i==3) next(err);
});
}
测试:
describe.only('Image Resizing module', function () {
var gmSubclassStub = sinon.stub();
var s3Stub = {};
var proxyquire = require('proxyquire');
var testedModule = proxyquire('../index', {
'gm': {subClass: sinon.stub().returns(gmSubclassStub)},
'aws-sdk': {"S3": sinon.stub().returns(s3Stub)}
});
describe('AwsHandler', function () {
var event = {};
// The done callback is used for async testing
it("should call gm write with correct files", function (done) {
// Arrange
var resizeStub = sinon.stub();
var buffer800Spy = sinon.stub().withArgs("jpg").callsArgWith(1, null, "800 buffer");
var buffer500Spy = sinon.stub().withArgs("jpg").callsArgWith(1, null, "500 buffer");
var buffer200Spy = sinon.stub().withArgs("jpg").callsArgWith(1, null, "200 buffer");
var buffer45Spy = sinon.stub().withArgs("jpg").callsArgWith(1, null, "45 buffer");
resizeStub.withArgs(800).returns({toBuffer: buffer800Spy});
resizeStub.withArgs(500).returns({toBuffer: buffer500Spy});
resizeStub.withArgs(200).returns({toBuffer: buffer200Spy});
resizeStub.withArgs(45).returns({toBuffer: buffer45Spy});
gmSubclassStub.withArgs("response body", "test.jpg").returns({resize: resizeStub});
s3Stub.getObject = sinon.stub()
.withArgs({name: "testbucket", key: "test.jpg"})
.callsArgWith(1, null, {
Body: "response body",
ContentType: "response content type"
});
var putObjectMock = sinon.mock();
s3Stub.putObject = putObjectMock;
putObjectMock.callsArgWith(1, null, {}); // return behaviour of the mock
putObjectMock.exactly(4); // sets expectation that it is called 4 times
// Act - this calls the tested method
testedModule.AwsHandler(event, {
done: function () {
// Assertions need to be inside callback because it is async
assert.deepEqual(putObjectMock.getCall(0).args[0], {
Bucket: "testbucket",
Key: "dst/large/test.jpg",
Body: "800 buffer",
ContentType: "response content type"
});
assert.deepEqual(putObjectMock.getCall(1).args[0], {
Bucket: "testbucket",
Key: "dst/medium/test.jpg",
Body: "500 buffer",
ContentType: "response content type"
});
assert.deepEqual(putObjectMock.getCall(2).args[0], {
Bucket: "testbucket",
Key: "dst/small/test.jpg",
Body: "200 buffer",
ContentType: "response content type"
});
assert.deepEqual(putObjectMock.getCall(3).args[0], {
Bucket: "testbucket",
Key: "dst/thumbnail/test.jpg",
Body: "45 buffer",
ContentType: "response content type"
});
// This ends the async test
done();
}
});
});
});
});
这种问题很难在这里回答;这个问题不是很具体,也不是一个可以用意见、想法等来回答的开放式问题
因此,我创建了一个解决 async.waterfall
问题的类似实现,并提供了一个测试 AwsHandler
覆盖率达到 100% 的测试。
代码在这里 gist,因为它比这里更方便和可读。
我也写了一个blog post与这个实现相关的