9

I wrote a lambda as follows.

handler.js

const aws = require('aws-sdk');
const dynamoDb = new aws.DynamoDB.DocumentClient();

const testHandler = async event => {
  // some code
  // ...
  const user = await getUser(userId)
  // ...
  // some code
}

const promisify = foo => new Promise((resolve, reject) => {
  foo((error, result) => {
    if (error) {
      reject(error)
    } else {
      resolve(result)
    }
  })
})

const getUser = (userId) => promisify(callback =>
  dynamoDb.get({
    TableName: 'test-table',
    Key: {
      "PK": `${userId}`,
      "SK": `${userId}`
    }
  }, callback))
  .then((user) => {
    console.log(`Retrieved user: ${userId}`)
    return user
  })


module.exports = {
  testHandler: testHandler,
  getUser: getUser
}

I want to write a unit test for testing the getUser function so I tried the following.

handler.test.js

const handler = require('../handler');
const AWS = require('aws-sdk')

const dynamoDbGetParameterPromise = jest.fn().mockReturnValue({
  promise: jest.fn().mockResolvedValue({
    PK: 'userId-123', SK: 'userId-123'
  })
})

AWS.DynamoDB.DocumentClient = jest.fn().mockImplementation(() => ({
  get: dynamoDbGetParameterPromise
}))


describe('test getUser', () => {

  beforeEach(() => {
    jest.resetModules()
  });

  test('get user success', async () => {
    const user = { PK: 'userId-123', SK: 'userId-123' };
    const result = await handler.getUser(userId);
    expect(result).toEqual(user);
  });
});

The error is as follows.

ConfigError: Missing region in config

      105 |
      106 | const getUser = (userId) => promisify(callback =>
    > 107 |   dynamoDb.get({
          |            ^
      108 |     TableName: 'test-table',
      109 |     Key: {
      110 |       "PK": 'userId-123',

It seems the test still uses the dynamoDb in the handler.js rather than the mocked in the test.

Any ideas on how to wire up the mock correctly to test the function? Thanks in advance!

Conan
  • 355
  • 1
  • 4
  • 11

2 Answers2

7

You can use jest's auto-mock by adding

jest.mock("aws-sdk");

and then AWS.DynamoDB.DocumentClient will be a mocked class so you'll be able to mock it's implementation. And since we want it's get method to be a function that accepts anything as a first argument (as we won't do anything with it within the mock implementation) and a callback that we're expecting it to have been called with null and user we can mock it like this:

AWS.DynamoDB.DocumentClient.prototype.get.mockImplementation((_, cb) => {
  cb(null, user);
});
Teneff
  • 30,564
  • 13
  • 72
  • 103
  • It works like a charm! Thank you very much for your answer! Can I ask a follow-up question? When I try the same pattern to mock another AWS call, `EventBridge.putEvents`, the `mockImplementation` throws the error `TypeError: Cannot read property 'mockImplementation' of undefined`. Then I have to use the jest.fn() instead. The following works: `aws.EventBridge.prototype.putEvents = jest.fn((_, cb) => {cb(null, {})}) ` Any idea why the `mockImplmentation` does not work if after a prototype like `aws.EventBridge.prototype.putEvents.mockImplementation`? – Conan Oct 28 '20 at 09:58
  • @Conan can you post a separate question with your approach and some example implementation ? – Teneff Oct 28 '20 at 10:06
  • 1
    Done posted the follow-up question here https://stackoverflow.com/questions/64571126/jest-mockimplementation-errors-when-mocking-aws-sdk-top-level-functions – Conan Oct 28 '20 at 10:41
  • 1
    OMG I couldn't thank you enough for the brilliant solution. Never really thought of mocking a member method in a class using the keyword `prototype`. And I also just figured out that using the same approach works for assertion too, i.e `expect(AWS.DynamoDB.DocumentClient.prototype.get).toBeCalled()` <3 – stephen Apr 07 '21 at 13:28
6

You could use jest.mock(moduleName, factory, options) to mock aws-sdk module manually.

E.g.

handler.js:

const aws = require('aws-sdk');
const dynamoDb = new aws.DynamoDB.DocumentClient();

const promisify = (foo) =>
  new Promise((resolve, reject) => {
    foo((error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });

const getUser = (userId) =>
  promisify((callback) =>
    dynamoDb.get(
      {
        TableName: 'test-table',
        Key: {
          PK: `${userId}`,
          SK: `${userId}`,
        },
      },
      callback,
    ),
  ).then((user) => {
    console.log(`Retrieved user: ${userId}`);
    return user;
  });

module.exports = { getUser };

handler.test.js:

const aws = require('aws-sdk');
const { getUser } = require('./handler');

jest.mock('aws-sdk', () => {
  const mDocumentClient = { get: jest.fn() };
  const mDynamoDB = { DocumentClient: jest.fn(() => mDocumentClient) };
  return { DynamoDB: mDynamoDB };
});
const mDynamoDb = new aws.DynamoDB.DocumentClient();

describe('64564233', () => {
  afterAll(() => {
    jest.resetAllMocks();
  });
  it('should get user', async () => {
    const mResult = { name: 'teresa teng' };
    mDynamoDb.get.mockImplementationOnce((_, callback) => callback(null, mResult));
    const actual = await getUser(1);
    expect(actual).toEqual({ name: 'teresa teng' });
    expect(mDynamoDb.get).toBeCalledWith(
      {
        TableName: 'test-table',
        Key: {
          PK: '1',
          SK: '1',
        },
      },
      expect.any(Function),
    );
  });

  it('should handler error', async () => {
    const mError = new Error('network');
    mDynamoDb.get.mockImplementationOnce((_, callback) => callback(mError));
    await expect(getUser(1)).rejects.toThrowError('network');
    expect(mDynamoDb.get).toBeCalledWith(
      {
        TableName: 'test-table',
        Key: {
          PK: '1',
          SK: '1',
        },
      },
      expect.any(Function),
    );
  });
});

unit test result:

 PASS  src/stackoverflow/64564233/handler.test.js (14.929s)
  64564233
    ✓ should get user (23ms)
    ✓ should handler error (3ms)

  console.log src/stackoverflow/64564233/handler.js:433
    Retrieved user: 1

------------|----------|----------|----------|----------|-------------------|
File        |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
------------|----------|----------|----------|----------|-------------------|
All files   |      100 |      100 |      100 |      100 |                   |
 handler.js |      100 |      100 |      100 |      100 |                   |
------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        17.435s

source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/64564233

Lin Du
  • 88,126
  • 95
  • 281
  • 483
  • Thank you very much for your comprehensive answer! There seems an issue with this solution that if the `handler.js` has another AWS variable, the test fails. e.g. If I put `const eventBridge = new aws.EventBridge();` before the DynamoDB in `handler.js`, the test fails with the error `TypeError: aws.EventBridge is not a constructor`. – Conan Oct 28 '20 at 10:09
  • Awesome example man. Thanks so much. It's much more easier to mock the class than to use some strange library to mock the whole AWS Sdk. It works fine for me – Pablo Lopes Mar 15 '21 at 22:41
  • Hey. Thanks a lot for your answer. It helped my issue as well. Do you happen to know why extracting `get: jest.fn()` in a const makes the `mockImplementationOnce` call work, but if you use it directly it doesn't? Not sure if my question makes sense, or how to add an entire example – iDaniel19 Nov 29 '22 at 17:49