Sinon.Stub в Node с AWS-SDK
Я пытаюсь написать какое-то тестовое покрытие для приложения, которое использует модуль aws-sdk
NPM, который подталкивает вещи к очереди SQS, но я не уверен, как правильно имитировать вещи.
Вот мой тест:
var request = require('superagent'),
expect = require('chai').expect,
assert = require('chai').assert,
sinon = require('sinon'),
AWS = require('aws-sdk'),
app = require("../../../../app");
describe("Activities", function () {
describe("POST /activities", function () {
beforeEach(function(done) {
sinon.stub(AWS.SQS.prototype, 'sendMessage');
done();
});
afterEach(function(done) {
AWS.SQS.prototype.sendMessage.restore();
done();
});
it("should call SQS successfully", function (done) {
var body = {
"custom_activity_node_id" : "1562",
"campaign_id" : "318"
};
reqest
.post('/v1/user/123/custom_activity')
.send(body)
.set('Content-Type', 'application/json')
.end(function(err, res) {
expect(res.status).to.equal(200)
assert(AWS.SQS.sendMessage.calledOnce);
assert(AWS.SQS.sendMessage.calledWith(body));
});
});
});
});
Ошибка, которую я вижу, это:
1) Activities POST /activities "before each" hook:
TypeError: Attempted to wrap undefined property sendMessage as function
2) Activities POST /activities "after each" hook:
TypeError: Cannot call method 'restore' of undefined
Я немного новичок, когда дело доходит до sinon.stub
или насмешливых объектов в JavaScript, поэтому, пожалуйста, извините мое невежество
Ответы
Ответ 1
Мы создали модуль aws-sdk-mock npm, который высмеивает все сервисы и методы AWS SDK. https://github.com/dwyl/aws-sdk-mock
Он очень прост в использовании. Просто позвоните AWS.mock с помощью сервиса, метода и функции заглушки.
AWS.mock('SQS', 'sendMessage', function(params, callback) {
callback(null, 'success');
});
Затем восстановите методы после тестирования, вызвав:
AWS.restore('SQS', 'sendMessage');
Ответ 2
Вот как я заглушил AWS-SDK, используя sinonjs
import AWS from 'aws-sdk'
import sinon from 'sinon'
let sinonSandbox
const beforeEach = (done) => {
sinonSandbox = sinon.sandbox.create()
done()
}
const afterEach = done => {
sinonSandbox.restore()
done()
}
lab.test('test name', (done) => {
sinonSandbox.stub(AWS, 'SQS')
.returns({
getQueueUrl: () => {
return {
QueueUrl: 'https://www.sample.com'
}
}
})
done()
})
В основном я контролирую все методы из основного SQS. Надеюсь, это поможет кому-то.
Ответ 3
Вы можете заглушить методы AWS SDK с помощью Sinon со следующими
-
Оберните экземпляр AWS SDK и разрешите его устанавливать извне:
//Within say, SqsService.js
var Aws = require('aws-sdk');
exports.sqsClient = new Aws.SQS({
region: <AWS_REGION>,
apiVersion: <API_VERSION>,
accessKeyId: <AWS_ACCESS_KEY_ID>,
secretAccessKey: <AWS_SECRET_KEY>
});
-
При использовании sqsClient
убедитесь, что вместо этого используется обернутый экземпляр.
var SqsService = require('./SqsService');
function (message, callback) {
//Do stuff..
//Then send stuff..
SqsService.sqsClient.sendMessage(message, callback);
});
-
Итак, изменив ваш тестовый пример сверху, используя обернутый AWS SDK:
var request = require('superagent'),
expect = require('chai').expect,
assert = require('chai').assert,
sinon = require('sinon'),
SqsService = require('./SqsService'), //Import wrapper
app = require("../../../../app");
describe("Activities", function () {
describe("POST /activities", function () {
var sendMessageStub;
beforeEach(function(done) {
//Stub like so here
sendMessageStub = sinon.stub(SqsService.sqsClient, 'sendMessage').callsArgWith(1, null, { MessageId: 'Your desired MessageId' });
done();
});
afterEach(function(done) {
sendMessageStub.restore();
done();
});
it("should call SQS successfully", function (done) {
var body = {
"custom_activity_node_id" : "1562",
"campaign_id" : "318"
};
reqest
.post('/v1/user/123/custom_activity')
.send(body)
.set('Content-Type', 'application/json')
.end(function(err, res) {
expect(res.status).to.equal(200)
assert(sendMessageStub.calledOnce);
assert(sendMessageStub.calledWith(body));
});
});
});
});
Ответ 4
Вы можете сделать это, не внося никаких дополнительных библиотек, используя что-то вроде этого:
const mocha = require('mocha'),
chai = require('chai'),
expect = chai.expect, // Using Expect style
sinon = require('sinon'),
AWS = require('aws-sdk');
describe('app', function () {
var aws, sqs, app,
sendMessageError = null,
sendMessageData = { MessageId: "1" };
before(() => {
// Create a stub for the SQS lib
sqs = sinon.stub({ sendMessage: Function() });
// Make sure that when someone calls AWS.SQS they get our stub
aws = sinon.stub(AWS, 'SQS');
aws.returns(sqs);
// Now include your app since it will `require` our stubbed version of AWS
app = require('./app');
});
after(() => {
aws.restore(); // Be kind to future tests
});
beforeEach(() => {
// Reset callback behavior after each test
sqs.sendMessage.reset();
// Call the callback supplied to sendMessage in the 1st position with the arguments supplied
sqs.sendMessage.callsArgWith(1, sendMessageError, sendMessageData);
});
it('sends messages', () => {
// Pretend you're using Promises in your app, but callbacks are just as easy
return app.sendMessage().then(() => {
const args = sqs.sendMessage.getCall(0).args[0];
expect(args.QueueUrl).to.be.eq('http://127.0.0.1/your/queue/url');
});
});
});
Ответ 5
Я не могу точно сказать, почему Sinon не может заглушить aws sdk (может быть, какой-то эксперт JS может объяснить это лучше), но он работает с proxyquire.
Proxies nodejs требуют, чтобы сделать переопределяющие зависимости во время тестирования легкими, оставаясь полностью ненавязчивыми.
Ответ 6
Я думаю, что проблема заключается в том, что классы AWS SDK динамически создаются из конфигурации JSON. Здесь для SQS: Github.
Все вызовы API в конечном итоге сводятся к makeRequest
или makeUnauthenticatedRequest
на Service, поэтому я просто опечатывал те, которые используют withArgs(...)
. Например:
var stub = sinon.stub(AWS.Service.prototype, 'makeRequest');
stub.withArgs('assumeRole', sinon.match.any, sinon.match.any)
.yields(null, fakeCredentials);
который отлично работал для моего простого использования.
Ответ 7
Мне нравится использовать обещания, опираясь на ответ @kdlcruz выше, я делаю что-то вроде этого:
import AWS from 'aws-sdk'
import sinon from 'sinon'
let sinonSandbox
const beforeEach = (done) => {
sinonSandbox = sinon.sandbox.create()
done()
}
const afterEach = done => {
sinonSandbox.restore()
done()
}
function mockAWSCall(service, method, expectedArgs, response) {
var stubDef = {};
stubDef[method] = function(args) {
if(expectedArgs) {
expect(args).to.deep.equal(expectedArgs);
}
return {
promise: () => {
return new Promise(function (resolve, reject) {
if(response.startsWith("ERROR:")) {
reject(response);
} else {
resolve(response);
}
});
}
};
};
sinonSandbox.stub(AWS, service).returns(stubDef);
}
lab.test('test name', (done) => {
mockAWSCall('SQS', 'sendMessage', {
MessageBody: 'foo', QueueUrl: 'http://xxx'
}, 'ok');
// Do something that triggers the call...
done()
})
Ответ 8
Я просто часами пытался заставить работать AWS SQS aws-sdk-mock
, не прибегая к требованию aws-sdk-mock
для импорта aws-sdk
внутри функции.
AWS.DynamoDB.DocumentClient
над AWS.DynamoDB.DocumentClient
было довольно легко, но издевательство над AWS.SQS
поставило меня в тупик, пока я не наткнулся на предложение использовать rewire.
Моя лямбда перемещает плохие сообщения в FQQueue SQS (вместо того, чтобы позволить лямбда-серверу отказывать и возвращать сообщение в обычную очередь для повторных попыток, а затем DeadLetterQueue после maxRetries). Модульные тесты были необходимы для проверки следующих методов SQS:
-
SQS.getQueueUrl
-
SQS.sendMessage
-
SQS.deleteMessage
Я постараюсь сохранить этот пример кода как можно более кратким, в то же время включив все соответствующие части:
Фрагмент моей AWS Lambda (index.js):
const AWS = require('aws-sdk');
AWS.config.update({region:'eu-west-1'});
const docClient = new AWS.DynamoDB.DocumentClient();
const sqs = new AWS.SQS({ apiVersion: '2012-11-05' });
// ...snip
Сокращенные записи событий лямбды (event.json)
{
"valid": {
"Records": [{
"messageId": "c292410d-3b27-49ae-8e1f-0eb155f0710b",
"receiptHandle": "AQEBz5JUoLYsn4dstTAxP7/IF9+T1S994n3FLkMvMmAh1Ut/Elpc0tbNZSaCPYDvP+mBBecVWmAM88SgW7iI8T65Blz3cXshP3keWzCgLCnmkwGvDHBYFVccm93yuMe0i5W02jX0s1LJuNVYI1aVtyz19IbzlVksp+z2RxAX6zMhcTy3VzusIZ6aDORW6yYppIYtKuB2G4Ftf8SE4XPzXo5RCdYirja1aMuh9DluEtSIW+lgDQcHbhIZeJx0eC09KQGJSF2uKk2BqTGvQrknw0EvjNEl6Jv56lWKyFT78K3TLBy2XdGFKQTsSALBNtlwFd8ZzcJoMaUFpbJVkzuLDST1y4nKQi7MK58JMsZ4ujZJnYvKFvgtc6YfWgsEuV0QSL9U5FradtXg4EnaBOnGVTFrbE18DoEuvUUiO7ZQPO9auS4=",
"body": "{ \"key1\": \"value 1\", \"key2\": \"value 2\", \"key3\": \"value 3\", \"key4\": \"value 4\", \"key5\": \"value 5\" }",
"attributes": {
"ApproximateReceiveCount": "1",
"SentTimestamp": "1536763724607",
"SenderId": "AROAJAAXYIAN46PWMV46S:[email protected]",
"ApproximateFirstReceiveTimestamp": "1536763724618"
},
"messageAttributes": {},
"md5OfBody": "e5b16f3a468e6547785a3454cfb33293",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:eu-west-1:123456789012:sqs-queue-name",
"awsRegion": "eu-west-1"
}]
}
}
Сокращенный файл модульного теста (test/index.test.js):
const AWS = require('aws-sdk');
const expect = require('chai').expect;
const LamdbaTester = require('lambda-tester');
const rewire = require('rewire');
const sinon = require('sinon');
const event = require('./event');
const lambda = rewire('../index');
let sinonSandbox;
function mockGoodSqsMove() {
const promiseStubSqs = sinonSandbox.stub().resolves({});
const sqsMock = {
getQueueUrl: () => ({ promise: sinonSandbox.stub().resolves({ QueueUrl: 'queue-url' }) }),
sendMessage: () => ({ promise: promiseStubSqs }),
deleteMessage: () => ({ promise: promiseStubSqs })
}
lambda.__set__('sqs', sqsMock);
}
describe('handler', function () {
beforeEach(() => {
sinonSandbox = sinon.createSandbox();
});
afterEach(() => {
sinonSandbox.restore();
});
describe('when SQS message is in dedupe cache', function () {
beforeEach(() => {
// mock SQS
mockGoodSqsMove();
// mock DynamoDBClient
const promiseStub = sinonSandbox.stub().resolves({'Item': 'something'});
sinonSandbox.stub(AWS.DynamoDB.DocumentClient.prototype, 'get').returns({ promise: promiseStub });
});
it('should return an error for a duplicate message', function () {
return LamdbaTester(lambda.handler)
.event(event.valid)
.expectReject((err, additional) => {
expect(err).to.have.property('message', 'Duplicate message: {"Item":"something"}');
});
});
});
});