4

I have a function which is accessing multiple aws resources and now need to test this function, but I don't know how to mock these resources.

I have tried following github of aws-sdk-mock, but didn't get much there.

function someData(event, configuration, callback) {

    // sts set-up
    var sts = new AWS.STS(configuration.STS_CONFIG);

    sts.assumeRole({
      DurationSeconds: 3600,
      RoleArn: process.env.CROSS_ACCOUNT_ROLE,
      RoleSessionName: configuration.ROLE_NAME
    }, function(err, data) {
      if (err) {
        // an error occurred
        console.log(err, err.stack);
      } else {
        // successful response

        // resolving static credential
        var creds = new AWS.Credentials({
          accessKeyId: data.Credentials.AccessKeyId,
          secretAccessKey: data.Credentials.SecretAccessKey,
          sessionToken: data.Credentials.SessionToken
        });

         // Query function
         var dynamodb = new AWS.DynamoDB({apiVersion: configuration.API_VERSION, credentials:  creds, region: configuration.REGION});
         var docClient = new AWS.DynamoDB.DocumentClient({apiVersion: configuration.API_VERSION, region: configuration.REGION, endpoint: configuration.DDB_ENDPOINT, service: dynamodb });

            // extract params
            var ID = event.queryStringParameters.Id;
            console.log('metrics of id ' + ID);

            var params = {
                TableName: configuration.TABLE_NAME,
                ProjectionExpression: configuration.PROJECTION_ATTR,
                KeyConditionExpression: '#ID = :ID',
                ExpressionAttributeNames: {
                    '#ID': configuration.ID
                },
                ExpressionAttributeValues: {
                    ':ID': ID
                }
            };

            queryDynamoDB(params, docClient).then((response) => {
                console.log('Params: ' + JSON.stringify(params));
                // if the query is Successful
                if( typeof(response[0]) !== 'undefined'){
                    response[0]['Steps'] = process.env.STEPS;
                    response[0]['PageName'] = process.env.STEPS_NAME;
                }
                console.log('The response you get', response);
                var success = {
                    statusCode: HTTP_RESPONSE_CONSTANTS.SUCCESS.statusCode,
                    body: JSON.stringify(response),
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    isBase64Encoded: false
                };
                return callback(null, success);
            }, (err) => {
                // return internal server error
                return callback(null, HTTP_RESPONSE_CONSTANTS.BAD_REQUEST);
            });
      }

    });

}

This is lambda function which I need to test, there are some env variable also which is being used here.

Now I tried writing Unit test for above function using aws-sdk-mock but still I am not able to figure out how to actually do it. Any help will be appreciated. Below is my test code

describe('test getMetrics', function() {

    var expectedOnInvalid = HTTP_RESPONSE_CONSTANTS.BAD_REQUEST;

    it('should assume role ', function(done){
        var event = {
          queryStringParameters : {
              Id: '123456'
          }
        };

        AWS.mock('STS', 'assumeRole', 'roleAssumed');
        AWS.restore('STS');
        AWS.mock('Credentials', 'credentials');
        AWS.restore('Credentials');
        AWS.mock('DynamoDB.DocumentClient', 'get', 'message');
        AWS.mock('DynamoDB', 'describeTable', 'message');
        AWS.restore('DynamoDB');
        AWS.restore('DynamoDB.DocumentClient');

        someData(event, configuration, (err, response) => {
            expect(response).to.deep.equal(expectedOnInvalid);
            done();
        });


    });


});

I am getting the following error :

{ MultipleValidationErrors: There were 2 validation errors:
* MissingRequiredParameter: Missing required key 'RoleArn' in params
* MissingRequiredParameter: Missing required key 'RoleSessionName' in params
ankuselfie
  • 65
  • 1
  • 1
  • 11

4 Answers4

2

I strongly disagree with @ttulka's answer, so I have decided to add my own as well.

Given you received an event in your Lambda function, it's very likely you'll process the event and then invoke some other service. It could be a call to S3, DynamoDB, SQS, SNS, Kinesis...you name it. What is there to be asserted at this point?

Correct arguments!

Consider the following event:

{
   "data": "some-data",
   "user": "some-user",
   "additionalInfo": "additionalInfo"
}

Now imagine you want to invoke documentClient.put and you want to make sure that the arguments you're passing are correct. Let's also say that you DON'T want the additionalInfo attribute to be persisted, so, somewhere in your code, you'd have this to get rid of this attribute

delete event.additionalInfo

right?

You can now create a unit test to assert that the correct arguments were passed into documentClient.put, meaning the final object should look like this:

 {
   "data": "some-data",
   "user": "some-user"
 }

Your test must assert that documentClient.put was invoked with a JSON which deep equals the JSON above.

If you or any other developer now, for some reason, removes the delete event.additionalInfo line, tests will start failing.

And this is very powerful! If you make sure that your code works the way you expect, you basically don't have to worry about creating integration tests at all.

Now, if a SQS consumer Lambda expects the body of the message to contain some field, the producer Lambda should always take care of it to make sure the right arguments are being persisted in the Queue. I think by now you get the idea, right?

I always tell my colleagues that if we can create proper unit tests, we should be good to go in 95% of the cases, leaving integration tests out. Of course it's better to have both, but given the amount of time spent on creating integration tests like setting up environments, credentials, sometimes even different accounts, is not worth it. But that's just MY opinion. Both you and @ttulka are more than welcome to disagree.

Now, back to your question:

You can use Sinon to mock and assert arguments in your Lambda functions. If you need to mock a 3rd-party service (like DynamoDB, SQS, etc), you can create a mock object and replace it in your file under test using Rewire. This usually is the road I ride and it has been great so far.

Thales Minussi
  • 6,965
  • 1
  • 30
  • 48
1

I see unit testing as a way to check if your domain (business) rules are met.

As far as your Lambda contains exclusively only integration of AWS services, it doesn't make much sense to write a unit test for it.

To mock all the resources means, your test will be testing only communication among those mocks - such a test has no value.

External resources mean input/output, this is what integration testing focuses on.

Write integration tests and run them as a part of your integration pipeline against real deployed resources.

ttulka
  • 10,309
  • 7
  • 41
  • 52
1

Try setting aws-sdk module explicitly.
Project structures that don't include the aws-sdk at the top level node_modules project folder will not be properly mocked. An example of this would be installing the aws-sdk in a nested project directory. You can get around this by explicitly setting the path to a nested aws-sdk module using setSDK().

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

For more details on this : Read aws-sdk-mock documentation, they have explained it even better.

Atul Kumar
  • 690
  • 1
  • 9
  • 20
0

This is how we can mock STS in nodeJs.

import { STS } from 'aws-sdk';

export default class GetCredential {
  constructor(public sts: STS) { }

  public async getCredentials(role: string) {
    this.log.info('Retrieving credential...', { role });

    const apiRole = await this.sts
      .assumeRole({
        RoleArn: role,
        RoleSessionName: 'test-api',
      })
      .promise();
    if (!apiRole?.Credentials) {
      throw new Error(`Credentials for ${role} could not be retrieved`);
    }

    return apiRole.Credentials;
  }
}

Mock for the above function

import { STS } from 'aws-sdk';
import CredentialRepository from './GetCredential'; 

const sts = new STS();    
let testService: GetCredential;

beforeEach(() => {
  testService = new GetCredential(sts);
});

describe('Given getCredentials has been called', () => {
  it('The method returns a credential', async () => {
    const credential = {
      AccessKeyId: 'AccessKeyId',
      SecretAccessKey: 'SecretAccessKey',
      SessionToken: 'SessionToken'
    };

    const mockGetCredentials = jest.fn().mockReturnValue({
      promise: () => Promise.resolve({ Credentials: credential }),
    });
    testService.sts.assumeRole = mockGetCredentials;
    const result = await testService.getCredentials('fakeRole');
    expect(result).toEqual(credential);
  });
});