How to mock AWS sqs call for unit testing

19,359

Solution 1

Here is the solution, you don't need aws-sdk-mock module, you can mock aws-sdk by yourself.

index.ts:

import AWS from 'aws-sdk';

const sqs = new AWS.SQS({
  region: 'us-east-1'
});

const sendMessage = async (msg, queueUrl) => {
  try {
    const params = {
      MessageBody: JSON.stringify(msg),
      QueueUrl: queueUrl
    };
    const res = await sqs.sendMessage(params).promise();
    return res;
  } catch (err) {
    console.log('Error:', `failed to send message ${err}`);
    throw new Error(err);
  }
};

export { sendMessage as default };

index.spec.ts:

import sendMessage from './';
import AWS from 'aws-sdk';

jest.mock('aws-sdk', () => {
  const SQSMocked = {
    sendMessage: jest.fn().mockReturnThis(),
    promise: jest.fn()
  };
  return {
    SQS: jest.fn(() => SQSMocked)
  };
});

const sqs = new AWS.SQS({
  region: 'us-east-1'
});

describe.only('Test case for SQS SendMessage', () => {
  beforeEach(() => {
    (sqs.sendMessage().promise as jest.MockedFunction<any>).mockReset();
  });
  it('should return the UserEvent', async () => {
    expect(jest.isMockFunction(sqs.sendMessage)).toBeTruthy();
    expect(jest.isMockFunction(sqs.sendMessage().promise)).toBeTruthy();
    (sqs.sendMessage().promise as jest.MockedFunction<any>).mockResolvedValueOnce('mocked data');
    const actualValue = await sendMessage('testURL', 'data');
    expect(actualValue).toEqual('mocked data');
    expect(sqs.sendMessage).toBeCalledWith({ MessageBody: '"testURL"', QueueUrl: 'data' });
    expect(sqs.sendMessage().promise).toBeCalledTimes(1);
  });

  it('should throw an error when send message error', async () => {
    const sendMessageErrorMessage = 'network error';
    (sqs.sendMessage().promise as jest.MockedFunction<any>).mockRejectedValueOnce(sendMessageErrorMessage);
    await expect(sendMessage('testURL', 'data')).rejects.toThrowError(new Error(sendMessageErrorMessage));
    expect(sqs.sendMessage).toBeCalledWith({ MessageBody: '"testURL"', QueueUrl: 'data' });
    expect(sqs.sendMessage().promise).toBeCalledTimes(1);
  });
});

Unit test result with 100% coverage:

 PASS  src/stackoverflow/57585620/index.spec.ts
  Test case for SQS SendMessage
    ✓ should return the UserEvent (7ms)
    ✓ should throw an error when send message error (6ms)

  console.log src/stackoverflow/57585620/index.ts:3137
    Error: failed to send message network error

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |      100 |      100 |                   |
 index.ts |      100 |      100 |      100 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        3.453s, estimated 6s

Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57585620

Solution 2

If you have a static sqs test message (for example in a unittest situation where you do hit sqs for some unavoidable reason), you could calculate the md5 sum by simply running the sendMessage against an actual SQS queue (make one quickly in some burner AWS Account, then log the response and md5sum the MessageBody object in the response.

In your unittest, you can then nock SQS simply by using

    const requestId = 'who';
    const messageId = 'wha';
    nock('https://sqs.eu-central-1.amazonaws.com')
        .post('/')
        .reply(
            200,
            `<SendMessageResponse><SendMessageResult><MD5OfMessageBody>193816d2f70f3e15a09037a5fded52f6</MD5OfMessageBody><MessageId>${messageId}</MessageId></SendMessageResult><ResponseMetadata><RequestId>${requestId}</RequestId></ResponseMetadata></SendMessageResponse>`,
        );

Do not forget to change your region and ofcourse the md5sum ;)

This method does not scale obviously, unless you calculate the messageBody's md5sum up front :)

Maybe it can help some folks with static unittest messages towards a quick fix.

Solution 3

The problem here is that the SQS service is initialized outside of the handler, ergo at the time the module is requested. As a result, the mock call will happen too late, as the service to be mocked (SQS in this case) was already created.

From the docs:

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

Updating your producer file as follows will correctly work with aws-sdk-mock:

const AWS = require('aws-sdk')

let sqs;

const sendMessage = async (msg, queueUrl) => {
  if(!sqs) {
    sqs = new AWS.SQS({
      region: 'us-east-1'
    });
  }
  try {
    const params = {
      MessageBody: JSON.stringify(msg),
      QueueUrl: queueUrl
    }
    const res = await sqs.sendMessage(params).promise()
    return res
  } catch (err) {
    console.log('Error:', `failed to send message ${err}`)
    throw new Error(err)
  }
}

export { sendMessage as default }
Share:
19,359
FLASH
Author by

FLASH

Updated on June 28, 2022

Comments

  • FLASH
    FLASH almost 2 years

    I'm using AWS SQS queue in a Node application and I have to write the unit test cases for the same. For that, I want to mock the SQS function call sendMessage() in the test files so, what should I do?

    I have tried using aws-sdk-mock but while making the call to the sendMessage(), the function is trying to connect to the Queue URL.

    Test File

    import AWSMock from 'aws-sdk-mock'
    import sendMessage from '../api/sqs/producer'
    
    describe.only('Test case for SQS SendMessage', () => {
      it('should return the UserEvent', async () => {
        AWSMock.mock('SQS', 'sendMessage', () => Promise.resolve('Success'))
        const res = await sendMessage('testURL', 'data')
        console.log('RES', res.response.data)
      })
    })
    

    Producer File

    const AWS = require('aws-sdk')
    
    const sqs = new AWS.SQS({
      region: 'us-east-1'
    })
    
    const sendMessage = async (msg, queueUrl) => {
      try {
        const params = {
          MessageBody: JSON.stringify(msg),
          QueueUrl: queueUrl
        }
        const res = await sqs.sendMessage(params).promise()
        return res
      } catch (err) {
        console.log('Error:', `failed to send message ${err}`)
        throw new Error(err)
      }
    }
    
    export { sendMessage as default }
    

    In above code, I'm expecting the Success as a return value in res

    Output

     FAIL  tests/sendMessage.test.js
      ● Console
    
        console.log api/sqs/producer/index.js:16
          Error: failed to send message UnknownEndpoint: Inaccessible host: `testurl'. This service may not b
    e available in the `us-east-1' region.
    
      ● Test case for SQS SendMessage › should return the UserEvent
    
        UnknownEndpoint: Inaccessible host: `testurl'. This service may not be available in the `us-east-1' r
    egion.
    
  • StLia
    StLia about 3 years
    Although this answer is perfect for that question, I would rethink this approach of testing. It's better if you mock the sqs so you can test your actual client. github.com/localstack/localstack is a tool that does so
  • shellscape
    shellscape almost 3 years
    There are many test runners other than jest, and to dismiss aws-sdk-mock as unneeded ignores that fact.
  • Jeremy D
    Jeremy D over 2 years
    This was really helpful and I was able to utilize the convention for other services I needed as mocks.
  • Lekens
    Lekens over 2 years
    Don't forget to require actual aws-sdk: return { ...jest.requireActual('aws-sdk'), SQS: jest.fn(() => SQSMocked), };