Jest has great built-ins for mocking, but mocking aws-sdk properly is quite tricky 😅. 
I wanted to write unit tests that work without performing network calls to AWS. 
I needed to both mock (change the behavior) and spy (assert on the changed methods) the SDK methods. 
At first, I tried a naive approach...
ReferenceError: Cannot access 'iotInstance' before initialization
The module factory of
jest.mock()is not allowed to reference any out-of-scope variables.
Invalid variable access: iotInstance
...and ended up with mysterious errors like this ☝️.
I googled for solutions, but the ones that worked, were just mocks, 
without a way to spy. 
I wanted tame this beast, because we use AWS SDK extensively. I decided to dig a little deeper.
Let's start with the code that we want to test
// SocketService.ts 
const config = require('config')
const AWS = require('aws-sdk')
// This is the AWS SDK part that we want to mock
const iotDataInstance = new AWS.IotData({
  endpoint: config.aws.iotEndpointHost,
  region: config.aws.iotAwsRegion,
  maxRetries: 0
})
class SocketService {
  static async publishNewVersion(projectId: string, version: string) {
    const params = {
      topic: `projects/${projectId}/versions`,
      payload: JSON.stringify({version}),
      qos: 0
    }
    // This is the part that we want to spy on
    await iotDataInstance
        .publish(params)
        .promise()
  }
}
module.exports = {SocketService}
It's a simplified version of one of many similar modules that I encounter in my day-job. This is part that causes problems:
const config = require('config')
const AWS = require('aws-sdk')
// Side-effect with no clean way to control it from outside 
// We need to mock this!
const iotDataInstance = new AWS.IotData({
  endpoint: config.aws.iotEndpointHost,
  region: config.aws.iotAwsRegion,
  maxRetries: 0
})
Similar side-effects make testing difficult (and may lead to unexpected results).
This is an anti-pattern, but it's a very common in the Node.js realm, and I wanted to learn how to deal with it. 
Idea: Using a dependency-injection container would solve this problem.
Another idea: We could also extract the AWS service instance to a "demilitarized zone"
Final Solution, the test suite:
// SocketService.spec.ts 
// No need to import aws-sdk in the test file, we will mock it!
// ⚠️ Mock instance needs to be initialized before the module-in-test is required, 
// otherwise will get this error: 
// "ReferenceError: Cannot access 'mockIotDataInstance' before initialization"
//
// ⚠️ Variable name is ALSO IMPORTANT! It has to start with 'mock', 
// otherwise we will get this error: 
// "ReferenceError (...)
// The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables.
// Invalid variable access: notMockIotDataInstance
// "
const mockIotDataInstance = {
  // Tip: you can use `mockReturnThis` with fluid API
  publish: jest.fn().mockReturnThis(), 
  promise: jest.fn().mockResolvedValue({})
}
// ⚠️ Importing the module-in-test needs to be placed AFTER 
// we initialize the mockInstance,
// We can also import the module after the jest.mock('aws-sdk', ...) call, 
// it doesn't matter
const {SocketService} = require('./SocketService')
// Here we inject the mock into the module
jest.mock('aws-sdk', () => ({
  // ⚠️ IotData cannot be an arrow function! must be either 
  // a function or a jest.fn. 
  // Otherwise we will get this error:
  // "TypeError: Aws.IotData is not a constructor"
  IotData: jest.fn(
      // Implementation can be anything (arrow, function, jest.fn)
      () => mockIotDataInstance
  )
}))
describe('SocketService', () => {
  beforeEach(() => {
    // ⚠️ Important: we cannot call "resetAllMocks" because it will 
    // reset the mocks inside mockIotDataInstance
    // For example the .promise() call would not work with
    jest.clearAllMocks()
  })
  afterAll(() => {
    // Remember to cleanup the mocks afterwards
    jest.restoreAllMocks()
  })
  describe('publishNewVersion', () => {
    test('publishes a message to project versions channel', async () => {
      const projectId = 'my-project-id'
      const myVersion = Math.random()
          .toFixed(8)
          .slice(2)
      await SocketService.publishNewVersion(projectId, myVersion)
      expect(mockIotDataInstance.publish).toHaveBeenCalledWith(
          expect.objectContaining({
            topic: `projects/${projectId}/versions`,
            payload: JSON.stringify({version: myVersion})
          }))
    })
  })
})
To make assertions in tests cases we need a mock IoTData instance (mockIotDataInstance in the code). 
Its critical that variable name that starts with mock
so that jest gives it a special treatment
and allows to reference them in the hoisted calls to jest.mock('aws-sdk', ...) 😱
// Initialize the mock instance before importing 
// the module-in-test (the mock instance will be used in the the side-effect)
const mockIotDataInstance = {
  publish: jest.fn().mockReturnThis(),
  promise: jest.fn().mockResolvedValue({})
}
// Import module-in-test 
const {SocketService} = require('./SocketService')
// Setup the mock
jest.mock('aws-sdk', () => ({
  IotData: jest.fn(() => mockIotDataInstance)
}))
Alternatives
You can use a dedicated module that makes mocking easier like aws-sdk-mock. 
I just prefer to use as little dependencies as possible, especially in bigger projects.
Thanks for reading 😇!
Originally posted on https://blog.jangrzesik.com/mocking-aws-sdk-with-jest/
 

 
    
Top comments (1)
Thank you. This is the only clean, sane approach I've found. The usual practice of importing aws-sdk into the test module is - IMO - very poor. I like your solution. Very elegant.