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"}');
                });
        });
    });
});