dwyl / aws-sdk-mock

:rainbow: AWSomocks for Javascript/Node.js aws-sdk tested, documented & maintained. Contributions welcome!
Apache License 2.0
1.1k stars 109 forks source link

Provide more examples #60

Open ifuyivara opened 7 years ago

ifuyivara commented 7 years ago

I'm really trying to use this module to test our app. But I can't wrap my head around how to actually use it in a real scenario where I need to test some of the services our app is using. Here is what I'm doing:

var AWSMock = require('aws-sdk-mock');
const Resolver = require('../services/resolver');

AWSMock.mock('SQS', 'receiveMessage', function (params, callback){
  callback(null, "successfully received message");
});

Resolver.start();

AWSMock.restore('SQS');

Resolver.start is a function inside the resolver service that starts the AWS sdk like so:

var AWS = require('aws-sdk');
this.sqs = options.sqs || new AWS.SQS({
    region: options.region || 'eu-west-1'
  });
this.sqs.receiveMessage(receiveParams, this._handleSqsResponseBound);

Nothing gets mocked and my receiveMessage doesn't get called.

I tried several things but just can't seem to get this to work. Am I ordering the includes wrong?

nelsonic commented 7 years ago

@ifuyivara a Pull Request with more examples would be very welcome!

aaronbruckner commented 7 years ago

@ifuyivara I created a PR with an example on how to use the lib with Sinon spies: https://github.com/dwyl/aws-sdk-mock/pull/61

I haven't used aws-sdk-mock before either. But after I figured this out it really made testing a breeze.

hassankhan commented 7 years ago

Does anyone have an example of using aws-sdk-mock with Jest? I'm trying but it doesn't seem to actually mock anything for me 😢

nathanielks commented 7 years ago

@hassankhan

/* global test, expect */
const AWS = require('aws-sdk-mock')
const AWS_SDK = require('aws-sdk')
const mod = require('../index')
const demoAttributeResponse = require('./fixtures/demo-get-instance-attribute.json')

test('retrieve and decode instance userdata', () => {
  AWS.mock('EC2', 'describeInstanceAttribute', demoAttributeResponse)
  const EC2 = new AWS_SDK.EC2({apiVersion: '2016-11-15'})
  const InstanceId = 'i-1234567890abcdef0'
  const userData = mod.getUserData(EC2, InstanceId)
  expect(userData).toContain('ECS_CLUSTER')
  AWS.restore()
})

I pass the service directly to the function instead of the function creating it itself. It's also important to create the service after the service method has been mocked:

  AWS.mock('EC2', 'describeInstanceAttribute', demoAttributeResponse)
  const EC2 = new AWS_SDK.EC2({apiVersion: '2016-11-15'})
ignotus0001 commented 6 years ago

Just echoing the need for real-world examples that test user-created code, not just ones that are empty or test that a mocked AWS service will return something when you call service "metadata functions" like describeInstanceAttribute.

I sense this is a very useful repo, I just can't figure out how to use it :-(

hoegertn commented 6 years ago

Can you elaborate on what kind of examples you need? Maybe create a PR with some user code and I will try to write the tests for it.

w- commented 6 years ago

@hoegertn I'd like to take you up on your offer.

I spent a few hours trying to get this to work. i'm using jest. At one point i also tried adding mock-require

I have a class that I instantiate and provides access to AWS cognito functions.

For example

import AWS from 'aws-sdk'

class Cognito {
    constructor(options) {
        // .....bunch of init code

        this.cognitoService = new AWS.CognitoIdentityServiceProvider({
            region: this.region,
            accessKeyId: this.accessKeyId,
            secretAccessKey: this.secretAccessKey
        })

        this.srp = new SRPClient(this.userPoolId.split('_')[1])

        AWS.config.region = this.region

    }

    async create(email, password = null, attributes = {}) {
        await this._adminCreateUser(email, password, attributes)
        return this.adminGetUser(email)
    }

    async _adminCreateUser(username, password = null, attributes = {}, messageAction = null) {        
        return new Promise((resolve, reject) => {
            // .....
            // ..........bunch of stuff
            // ....
            this.cognitoService.adminCreateUser(params, (err, data) => {                
                if (err) {
                    reject(new CognitoError('create', err))
                } else {
                    resolve(data)
                }
            })
        })
    }

    async adminGetUser(username) {
        return new Promise((resolve, reject) => {
            // .....
            // ..........bunch of stuff
            // ....
            this.cognitoService.adminGetUser(params, async (userErr, user) => {
                if (userErr) {
                    reject(userErr)
                } else {
                    const { Groups } = await this.getGroupsForUser(user.Username)
                    user.groups = Groups
                    resolve(user)
                }
            })
        })
    }
}

so in this example I have

I basically couldn't even get the actual mocking of the AWS module to happen.

If there is an example of how to do this somewhere, i'd appreciate it if you could point me in the correct direction. Thanks

hoegertn commented 6 years ago

I will have a look at it. Is there a special reason you do not use the promise() method of the AWS client objects instead of creating your own Promises?

awsClient.method(params).promise()

w- commented 6 years ago

I will have a look at it. Is there a special reason you do not use the promise() method of the AWS client objects instead of creating your own Promises?

No. I didn't write the library and am not very familiar with it.
I'll take a look at how we might use the AWS client object promises.

After I posted, I kept researching and found an obscure forum post somewhere with someone posting some sample code they ddi for DynamoDB.
They have a similar setup except that for their library, instead of instantiating a AWS client object in the constructor, they pass one in as a parameter.

I may attempt refactoring the existing code to do this instead of how i've put it in the example but I have to get buy in as the module is already being used elsewhere..

rsmolkin commented 6 years ago

I'm also trying to figure out how to mock receiving an SQS Message from SNS Topic subscription then mock downloading a file from S3 with the file in the message.

trycatchjames commented 5 years ago

Alright so I got this library working, I was just a bit confused on how it works. It turned out my issue was from ES6/webpack. The library needs to know the location of the AWS lib or you need to give it an instance of AWS to apply the mocks to.

The documentation could benefit from a hello world block right at the top so you understand how you're meant to use it. It's got stacks of useful info in there but the first time you try to use the lib, it's a bit confusing. It could also be that JS isn't my primary language.

Test code:

import AWS from 'aws-sdk-mock';
import * as dynamoDbLib from "../src/libs/dynamodbLib";

test('foo', async () => {

  // This is the key line I was missing
  AWS.setSDKInstance(dynamoDbLib.getAWS());

  AWS.mock('DynamoDB.DocumentClient', 'get', function (params, callback) {
    callback(null, { Item: { Key: 'Value' } });
  });

  const params = {
    TableName: 'stop-mocking-me',
    Key: {
      id: 1234,
    }
  };

  let result = await dynamoDbLib.call("get", params);
  expect(result.Item).toEqual({ Key: 'Value' });

  AWS.restore('DynamoDB.DocumentClient');
});

My lib file from the serverless tutorial with an extra function:

import AWS from "aws-sdk";

AWS.config.update({ region: "ap-southeast-2" });

export function call(action, params) {
  const dynamoDb = new AWS.DynamoDB.DocumentClient();

  return dynamoDb[action](params).promise();
}

export function getAWS() {
  return AWS;
}
aaronbruckner commented 5 years ago

If you are using jest, you can create your own fully mocked instances of the AWS SDK without AWS-sdk-mock. I mix sinon spies/stubs into jest as they are a lot more functional than Jest mocks however this will work with jest mocks as well. The code below also shows how to return synchronous promises that you can then invoke after running test code to control how the mock AWS SDK responses.

import * as sinon from 'sinon';
import * as AWS from 'aws-sdk';
import { SynchronousPromise } from 'synchronous-promise';

const mockDocumentClient = {
    batchWrite: sinon.stub().callsFake(() => {
        return {
            promise: sinon.spy(SynchronousPromise.unresolved)
        };
    }),
    update: sinon.stub().callsFake(() => {
        return {
            promise: sinon.spy(SynchronousPromise.unresolved)
        };
    })
};

jest.mock('aws-sdk', () => {
    return {
        DynamoDB: {
            DocumentClient: () => {
                return mockDocumentClient;
            }
        }
    }
});

describe('Module that uses AWS SDK', () => {
    it('example', () => {
         moduleUnderTest.test();
         mockDocumentClient.update.returnValues[0]
             .promise.returnValues[0].resolve({FakeDynamoDBResponse: 'Make Me Real'});
    });
}
jontroncoso commented 5 years ago

holy crap. can't believe I got this working! I'm using classes and thought this would be impossible due to constraints in documentation. I got it working with "rewire", "unit.js" and "aws-sdk-mock"

# In the Test
'use strict';
const AWS = require('aws-sdk');
const AWSMock = require('aws-sdk-mock');
const test = require('unit.js');
const rewire = require('rewire');
// const path = require('path');

const sampleEvent = {
  // ... sensitive data
};
const sampleContext = {
  // ... sensitive data
};

process.env.S3_BUCKET = 'yourbucket.domain.com';
process.env.SNS_TOPIC_ERROR = 'error::Arn';
process.env.SNS_TOPIC_SUCCESS = 'Succees::Arn';

// const realAWS = etlFinishedNormal.getAws();
AWSMock.setSDKInstance(AWS);
// AWSMock.setSDK(path.resolve('node_modules/aws-sdk'));

describe('Tests the ETLFinished lambda function', function() {
  it('verifies moveFilesFromEtlToRaw references our dummy file', async () =>  {
    const dummyResponse = ['cheeseburger'];
    try {
      AWSMock.mock('S3', 'getObject', function (params, callback){
        console.log('------------- IT WORKS ------------');
        console.log(params);
        // console.log(process.env.S3_BUCKET);
        test.assert(params.Bucket === process.env.S3_BUCKET);
        callback(null, dummyResponse);
      });

      // const etlFinishedNormal = require('../../lambda/etlFinished/index.js');
      const etlFinishedIndex = rewire('../../lambda/etlFinished/index.js');
      const ETLFinished = etlFinishedIndex.__get__('ETLFinished');
      const etlFinished = new ETLFinished(sampleEvent, sampleContext);

      let result = await etlFinished.moveFilesFromEtlToRaw();
      test.assert(result === dummyResponse);
    } catch(error) {
      throw error;
      // done(error);
    }
  });

});
# The lambda function
const AWS = require('aws-sdk');
const sns = new AWS.SNS();
const s3 = new AWS.S3();

class ETLFinished {

  constructor(lambdaEvent, lambdaContext) {
    this.snsTopicSuccess = process.env.SNS_TOPIC_SUCCESS;
    this.snsTopicError = process.env.SNS_TOPIC_ERROR;
    this.bucket = process.env.S3_BUCKET;

    this.lambdaEvent = lambdaEvent;
    this.lambdaContext = lambdaContext;

    // Example JobName: 'fis-monetary'.
    [this.processor, this.jobName] = lambdaEvent.detail.jobName.split('-');

    // this.moveFilesFromEtlToRaw.bind(this)
  }

  async moveFilesFromEtlToRaw() {
    const s3ListObjectsParams = {
      Bucket: this.bucket,
      // Prefix: ['etl', this.processor, this.jobName].join('/'),
      Key: 'etl/foo/bar/whatever.txt'
    };
    console.log('s3ListObjectsParams: ', s3ListObjectsParams);

    return new Promise((resolve, reject) => {
      s3.getObject(s3ListObjectsParams, (err, data) => {
        if (err) reject(console.log('ERR: ', err, err.stack)); // an error occurred

        console.log('data: ', data);
        resolve(data);
      });
    });
  }
}
ericstiles commented 5 years ago

Could someone provide a simple example for testing response from IAM.listUsers()?

sevoGrigoryan2 commented 5 years ago

Example with Jest `'use strict';

// Generated by serverless-jest-plugin import uuid from 'uuid/v1';

const AWS = require('aws-sdk'); const AWSMock = require('aws-sdk-mock');

AWSMock.setSDKInstance(AWS);

const message = { Messages: [ { Body: JSON.stringify({ category: 'business', requestType: 'create' }) } ] };

AWSMock.mock('SQS', 'receiveMessage', (params, callback) => { callback(null, message); });

const mod = require('./../controllers/delivery/transactions-sns'); const jestPlugin = require('serverless-jest-plugin');

const { lambdaWrapper } = jestPlugin; const wrapped = lambdaWrapper.wrap(mod, { handler: 'handler' });

describe('TransactionsSns', () => { beforeAll((done) => { done(); });

it('test that ok response structure is correct', async () => { return wrapped.run().then((response) => { console.log('success response: ', response); expect(response).toMatchObject({ statusCode: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': true }, body: JSON.stringify({}) }); }); }, 50000);

afterAll(() => { AWSMock.restore('SQS', 'receiveMessage'); }); }); `

alvis commented 5 years ago

A trick to make aws-sdk-mock compatible with real-world example is to pass the SDK location to AWSMock.setSDK before loading anything. Example below is written in typescript but you can easily convert it to a javascript equivalent:

import AWSMock from 'aws-sdk-mock';

// load aws-sdk in ../model/dynamodb
import { DataStore } from '../model/dynamodb';

// make aws-sdk mockable everywhere
AWSMock.setSDK(resolve(__dirname, '..', 'node_modules', 'aws-sdk'));

describe('model:dynamodb', () => {
  // accept any write to DynamoDB
  AWSMock.mock('DynamoDB.DocumentClient', 'put', (_: any, callback: Function) =>
    callback()
  );

  test('createItinerary', async () => {
    const store = new DataStore();
   /* tests below */
  });
});
keslerm commented 4 years ago

@alvis thank you for this, from the documentation it's not immediately clear that you need to do this and it finally solved my issue.

iteles commented 4 years ago

🎉 It would be amazing if someone would like to submit a PR to the documentation to make things clearer 😊

teemuniiranen commented 4 years ago

I struggled myself with the client initialising because my service was initialised outside the function being tested and reused by multiple different functions that the module exported.

From README: "NB: The AWS Service needs to be initialised inside the function being tested in order for the SDK method to be mocked"

So to be able to mock and unit test you need to do it this way but is there a big performance difference?

@alvis I bet your DataStore() function includes "new AWS.DynamoDB.DocumentClient();" right?

alvis commented 4 years ago

@teemuniiranen correct! My DataStore is a just class which has a private new AWS.DynamoDB.DocumentClient()

gavinharris-dev commented 4 years ago

Thank you @teemuniiranen So for me, I had to do 2 things;

In my test file add: AWS.setSDKInstance(require('aws-sdk'));

And then, in my API, move the initialization of new AWS.S3(...) to be within the method being tested. I am not sure on the performance implications of this (as mentioned), but it works!

luismramirezr commented 4 years ago

A trick to make aws-sdk-mock compatible with real-world example is to pass the SDK location to AWSMock.setSDK before loading anything. Example below is written in typescript but you can easily convert it to a javascript equivalent:

import AWSMock from 'aws-sdk-mock';

// load aws-sdk in ../model/dynamodb
import { DataStore } from '../model/dynamodb';

// make aws-sdk mockable everywhere
AWSMock.setSDK(resolve(__dirname, '..', 'node_modules', 'aws-sdk'));

describe('model:dynamodb', () => {
  // accept any write to DynamoDB
  AWSMock.mock('DynamoDB.DocumentClient', 'put', (_: any, callback: Function) =>
    callback()
  );

  test('createItinerary', async () => {
    const store = new DataStore();
   /* tests below */
  });
});

So I tried this and didn't work. I'm using a class to initiate the service. After a lot of struggling to make it work, I found that you can't import the service directly: you must import AWS:

// Doesn't work
import SES from 'aws-sdk/clients/ses';

//This works
import AWS from 'aws-sdk';

const { SES } = AWS;

Full example (Typescript):

// File-to-test.ts
import AWS from 'aws-sdk';
import { Template } from 'aws-sdk/clients/ses';

export default class SESService {
  private client: AWS.SES;

  constructor() {
    this.client = new AWS.SES({
      region: 'us-east-1',
    });
  }

  async createTemplate(data: Template): Promise<boolean> {
    try {
      const response = await this.client
        .createTemplate({ Template: data })
        .promise();
      console.log(response);
      return true;
    } catch (error) {
      console.log(error);
      return false;
    }
  }
}
// File-to-test.spec.ts
import AWS from 'aws-sdk-mock';
import SESService from '@services/File-to-test.ts';
import { Template } from 'aws-sdk/clients/ses';

AWS.setSDK(resolve(__dirname, '..', '..', '..', 'node_modules', 'aws-sdk'));

AWS.mock('SES', 'createTemplate', () => Promise.resolve(true));

describe('SES Service', () => {

  it('should be able to create a new template in AWS', async () => {

    const importHTMLService = new SESService();

    const TemplateData: Template = {
      TemplateName: 'TemplateTest',
      HtmlPart: '<html><body><h1>Hello World!</h1></body></html>',
      SubjectPart: 'Test subject',
    };

    const response = await SESService.createTemplate(TemplateData);

    expect(response).toBe(true);
  });
});
Buuntu commented 4 years ago
test('foo', async () => {

  // This is the key line I was missing
  AWS.setSDKInstance(dynamoDbLib.getAWS());

  AWS.mock('DynamoDB.DocumentClient', 'get', function (params, callback) {
    callback(null, { Item: { Key: 'Value' } });
  });

  const params = {
    TableName: 'stop-mocking-me',
    Key: {
      id: 1234,
    }
  };

  let result = await dynamoDbLib.call("get", params);
  expect(result.Item).toEqual({ Key: 'Value' });

  AWS.restore('DynamoDB.DocumentClient');
});

How would you adapt this to work within an express route? I've tried awaiting a response using chai but I keep getting various errors and timeouts.

I've replicated the example and it does seem to work.

This is a non-working example that calls a route:

import * as dynamoDbLib from "dynamoDbLib";

router.get('/', (req, res) => {
  const params = {
    TableName: 'some_table',
  };

  dynamoDbLib.call('scan', params, function (err, data) {
    if (err) {
      res.status(500).send({
        success: false,
        message: 'Error: Server error',
      });
    } else {
      const { Items } = data;
      res.send({
        success: true,
        message: 'Loaded users',
        users: Items,
      });
    }
  });
});

the test I have is:

import * as dynamoDbLib from "dynamoDbLib";
import AWSMock from 'aws-mock-sdk';

it('should GET /', async () => {
  AWSMock.setSDKInstance(dynamoDbLib.getAws());

  AWSMock.mock('DynamoDB.DocumentClient', 'scan', function (params, callback) {
    callback(null, { Items: { Key: 'Value' } });
  });

  const res = await chai.request(app).get('/');
  expect(res.body).not.to.be.empty;
  AWSMock.restore('DynamoDB');
});

and the error:

Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.

I've also tried making the route async and adding the done argument to the test, neither works.

aesimano commented 3 years ago

Just a quick note that was easy for me to glance over and was critical for TypeScript:

// Does not work, which is what the documentation states:
import * as AWS from 'aws-sdk';

// Changing the line above to this got it working for me:
import AWS from 'aws-sdk';

...

// Later in my code...
AWSMock.setSDKInstance(AWS);
AWSMock.mock('SQS', 'receiveMessage', () => Promise.resolve(mockS3Event));

After fixing this my mocks started working.

jkeys-ecg-nmsu commented 3 years ago

@aesimano thank you for your note! Saved me several hours of banging my head against the wall.