How to mock and spy on AWS-SDK calls with jest
Tags: javascript, aws, jest, testing
Published: 2022-02-21 20:58:30

---
title: How to mock and spy on AWS-SDK calls with jest
published: true
description: Short guide on how to mock and spy on AWS SDK calls with jest and avoid common pitfalls
tags: 
  - javascript
  - aws
  - jest
  - testing

//cover_image: https://direct_url_to_image.jpg
---

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](https://lucasamos.dev/articles/jestmocking/), 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
```typescript
// 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:

```typescript
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:
```typescript
// 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](https://jestjs.io/docs/es6-class-mocks#calling-jestmock-with-the-module-factory-parameter)
and allows to reference them in the hoisted calls to `jest.mock('aws-sdk', ...)` 😱

```typescript
// 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](https://github.com/dwyl/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/](https://blog.jangrzesik.com/mocking-aws-sdk-with-jest/)