29

I am trying to write some test coverage for an app that uses the aws-sdk NPM module that pushes things up to a SQS queue, but I am unsure how to mock things correctly.

Here is my test so far:

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

    });

});

The error I am seeing is:

  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

I am a bit of a newb when it comes to sinon.stub or mocking objects in JavaScript, so please excuse my ignorance

dennismonsewicz
  • 25,132
  • 33
  • 116
  • 189
  • Have you managed to find a solution to this yet? – hyprstack May 21 '15 at 14:10
  • @hyprstack have seen/tried **aws-sdk-mock** npm module ? (*see answer below*) – nelsonic Feb 12 '16 at 10:12
  • @nelsonic at the time I managed to stub the service with [proxyquire](https://github.com/thlorenz/proxyquire) and sinon and got it working. I haven't had a look at the **aws-sdk-mock** yet. Have you used it? – hyprstack Feb 12 '16 at 10:35
  • @hyprstack yes, we are using **aws-sdk-mock** (*which simplifies the `Sinon.Stub`*) :-) – nelsonic Feb 12 '16 at 11:23

9 Answers9

28

This is how I stub the AWS-SDK using 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()
})

Basically I control all the methods from the main SQS. Hope this will help someone

kdlcruz
  • 1,368
  • 13
  • 20
  • 6
    Not sure why this answer didn't get more love, it's simple and does the job. I've created a generalization of it to handle promise pattern (which I like to use) - see below – Jason Mar 13 '18 at 15:41
24

We have created an aws-sdk-mock npm module which mocks out all the AWS SDK services and methods. https://github.com/dwyl/aws-sdk-mock

It's really easy to use. Just call AWS.mock with the service, method and a stub function.

AWS.mock('SQS', 'sendMessage', function(params, callback) {
    callback(null, 'success');
});

Then restore the methods after your tests by calling:

AWS.restore('SQS', 'sendMessage');
Nikhila Ravi
  • 301
  • 2
  • 7
7

I think the issue is that AWS SDK classes are built dynamically from JSON configuration. Here's the one for SQS: Github.

All API calls eventually make it down to makeRequest or makeUnauthenticatedRequest on Service, so I just stubbed those using withArgs(...). For example:

var stub = sinon.stub(AWS.Service.prototype, 'makeRequest');
stub.withArgs('assumeRole', sinon.match.any, sinon.match.any)
    .yields(null, fakeCredentials);

which worked fine for my simple use case.

David
  • 149
  • 1
  • 2
  • 1
    is there anything for Lambda – kailash yogeshwar Jul 07 '18 at 11:52
  • Finally after spending hours, something which worked for me. Also that is the most generic thing for mocking/stubbing as it will cover all aws services.Not sure why its's not recognized enough.Man you deserve a medal for this. – lone_worrior Jun 24 '19 at 20:38
  • Perfect! The JSON definition are definitely the issue. I could not find a way to stub the SQS prototype since the functions are only bound to instances. – Peter Chaula Jun 30 '20 at 07:34
4

You can stub the AWS SDK methods with Sinon with the following

  1. Wrap the AWS SDK instance and allow the this to be set externally:

    //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>
    });
    
  2. When using the sqsClient, ensure to use the wrapped instance instead.

    var SqsService = require('./SqsService');
    
    function (message, callback) {
        //Do stuff..
        //Then send stuff..
        SqsService.sqsClient.sendMessage(message, callback);
    });
    
  3. So modifying your test case from above, using the wrapped 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));
                });
            });
        });
    });
    
gyamana
  • 1,202
  • 1
  • 16
  • 28
  • That's right, the issue is that Sinon has trouble trying to stub/spy the AWS SDK, so one solution is to stub/spy the wrapper method. It wasn't so ugly for us as we already had an existing method so that we didn't have to keep initialising the SDK with keys and what not – gyamana Jun 14 '16 at 02:53
4

You can do it without bringing in any extra libraries using something like this:

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');
        });
    });
});
Joel B
  • 12,082
  • 10
  • 61
  • 69
  • 1
    One-liner example for Kinesis publishing: `sinon.stub(AWS, 'Kinesis').returns({ putRecord: sinon.stub().callsArgWith(1, null, true) })` – noetix Nov 17 '17 at 05:48
2

I can't tell you exactly why it is not possible with Sinon to stub the aws sdk (maybe some JS expert can explain that better) but it works with proxyquire very well.

Proxies nodejs's require in order to make overriding dependencies during testing easy while staying totally unobstrusive.

1

I like to use promises, building upon @kdlcruz's answer above, I do something like this:

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()
})
Jason
  • 941
  • 1
  • 10
  • 19
0

With AWS SDK v3 it's become much easier. It even works with promises directly without having to create embedded stubbed objects.

  sinon.stub(SQS.prototype, 'sendMessage').resolves({
    SequenceNumber: '0',
  });

  const sqs = new SQS({});
  const result = await sqs.sendMessage({
    MessageBody: '',
    QueueUrl: '',
  });

  expect(SQS.prototype.sendMessage).to.be.calledOnce;
  expect(result.SequenceNumber).to.be('0');
Stephen Horvath
  • 5,188
  • 3
  • 24
  • 31
0

If you don't want to control at new aws.SNS, you can do this way as well for example:

const dummysns = new aws.SNS()
sandbox.stub(dummysns.constructor.prototype, 'publish').callsFake

// Now call the function which does the actual sns.publish
// Since it uses the same prototype, it calls the mocked function than the original one
Royal Pinto
  • 2,869
  • 8
  • 27
  • 39