Aws-sdk-js: Mock S3.PutObject Inside Promise Using Jest

Created on 29 Jan 2020  路  4Comments  路  Source: aws/aws-sdk-js

Is there a way to mock the s3.putObject inside a promise using Jest?

// Service

module.exports.functionUnderTest = async function functionUnderTest() {
  return await resolvePromise();
};

function resolvePromise() {
  return new Promise((resolve, reject) => {
    const params = {
      Bucket: "BUCKET_NAME",
      Key: "key",
      Body: "1,2,3"
    };

    s3.putObject(params, function(err, data) {
      if (err) reject(err);
      else resolve(`Success`);
    });
  });
}

## Test

const mockS3PutObject = jest.fn();

jest.mock("aws-sdk", () => {
  return {
    S3: jest.fn(() => ({
      putObject: mockS3PutObject
    }))
  };
});

test("My test", async () => {
    mockS3PutObject.mockImplementation(params => {
      return {
        Body: "test document"
      };
    });
    expect(await s3Service.functionUnderTest()).toEqual("test document");
  });

Running the test will result in the following:

Timeout - Async callback was not invoked within the 120000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 120000ms timeout specified by jest.setTimeout.Error:

Appreciate your help!

closing-soon guidance

Most helpful comment

This is the mocker I made for mocking the aws-sdk in Jest:

type AWSClientMap = typeof import('aws-sdk/clients/all');

interface MockReturn {
  promise: () => unknown;
}

/** Map of `aws-sdk` clients to their mocked endpoints */
const clientMocks: { [K in keyof AWSClientMap]?: unknown } = {};

export const mockAwsClientEndpoints = <
  TClient extends keyof AWSClientMap,
  TEndpoint extends keyof InstanceType<AWSClientMap[TClient]>,
  TMockedEndpoints extends Partial<Record<TEndpoint, jest.Mock>>
>(
  client: TClient,
  mockedEndpoints: TMockedEndpoints
): TMockedEndpoints => {
  const clientProxy = new Proxy(class {}, {
    construct(): object {
      return clientProxy;
    },
    get(_, property: TEndpoint & string): (...args: unknown[]) => MockReturn {
      const endpointMock = mockedEndpoints[property];

      if (endpointMock) {
        return (...args: unknown[]): MockReturn => ({
          promise: (): unknown => endpointMock(...args)
        });
      }

      throw new Error(`${client}#${property} was called without being mocked`);
    }
  });

  clientMocks[client] = clientProxy;

  return mockedEndpoints;
};

jest.mock('aws-sdk', () => clientMocks);

It's written in TypeScript, but should work the same if you remove all the types :)

(Here's an example of how to use it](https://github.com/G-Rath/terraport/blob/master/test/src/collectors/aws/collectRoute53ZoneDetails.spec.ts#L6-L10):

import { mockAwsClientEndpoints } from '@test/setupAwsSdkMock';
import { Route53 } from 'aws-sdk';

const {
  getHostedZone: getHostedZoneMock //
} = mockAwsClientEndpoints('Route53', {
  getHostedZone: jest.fn<Promise<Route53.GetHostedZoneResponse>, unknown[]>()
});

describe('my-test', () => {
  const HostedZone: Route53.HostedZone = {
    Id: 'HostedZone/Z123456789',
    Name: 'my.zone.com.',
    CallerReference: 'reference'
  };

  beforeEach(() => getHostedZoneMock.mockResolvedValue({ HostedZone }));
}

It's designed to be completely TypeScript compatible, which other mocks of the sdk typically aren't.
It's working well in my project, but I'd love feedback on if it works well for you :)

All 4 comments

This is the mocker I made for mocking the aws-sdk in Jest:

type AWSClientMap = typeof import('aws-sdk/clients/all');

interface MockReturn {
  promise: () => unknown;
}

/** Map of `aws-sdk` clients to their mocked endpoints */
const clientMocks: { [K in keyof AWSClientMap]?: unknown } = {};

export const mockAwsClientEndpoints = <
  TClient extends keyof AWSClientMap,
  TEndpoint extends keyof InstanceType<AWSClientMap[TClient]>,
  TMockedEndpoints extends Partial<Record<TEndpoint, jest.Mock>>
>(
  client: TClient,
  mockedEndpoints: TMockedEndpoints
): TMockedEndpoints => {
  const clientProxy = new Proxy(class {}, {
    construct(): object {
      return clientProxy;
    },
    get(_, property: TEndpoint & string): (...args: unknown[]) => MockReturn {
      const endpointMock = mockedEndpoints[property];

      if (endpointMock) {
        return (...args: unknown[]): MockReturn => ({
          promise: (): unknown => endpointMock(...args)
        });
      }

      throw new Error(`${client}#${property} was called without being mocked`);
    }
  });

  clientMocks[client] = clientProxy;

  return mockedEndpoints;
};

jest.mock('aws-sdk', () => clientMocks);

It's written in TypeScript, but should work the same if you remove all the types :)

(Here's an example of how to use it](https://github.com/G-Rath/terraport/blob/master/test/src/collectors/aws/collectRoute53ZoneDetails.spec.ts#L6-L10):

import { mockAwsClientEndpoints } from '@test/setupAwsSdkMock';
import { Route53 } from 'aws-sdk';

const {
  getHostedZone: getHostedZoneMock //
} = mockAwsClientEndpoints('Route53', {
  getHostedZone: jest.fn<Promise<Route53.GetHostedZoneResponse>, unknown[]>()
});

describe('my-test', () => {
  const HostedZone: Route53.HostedZone = {
    Id: 'HostedZone/Z123456789',
    Name: 'my.zone.com.',
    CallerReference: 'reference'
  };

  beforeEach(() => getHostedZoneMock.mockResolvedValue({ HostedZone }));
}

It's designed to be completely TypeScript compatible, which other mocks of the sdk typically aren't.
It's working well in my project, but I'd love feedback on if it works well for you :)

Hey @cedricsarigumba are you able to use the code provided by @G-Rath? It seems to be accurate.

@G-Rath @ajredniwja

Thanks for the recommendation but I ended up updating the source code by removing the enclosing promise. After that, the mock works! Please see below.

// From
function resolvePromise() {
  return new Promise((resolve, reject) => {
    const params = {
      Bucket: "BUCKET_NAME",
      Key: "key",
      Body: "1,2,3"
    };

    s3.putObject(params, function(err, data) {
      if (err) reject(err);
      else resolve(`Success`);
    });
  });
}

// To
async function resolvePromise() {
    const params = {
      Bucket: "BUCKET_NAME",
      Key: "key",
      Body: "1,2,3"
    };

    return await s3.putObject(params).promise();
}

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs and link to relevant comments in this thread.

Was this page helpful?
0 / 5 - 0 ratings