Search code examples
typescriptamazon-web-servicesamazon-s3ts-jestaws-sdk-js

How do I mock AWS S3 GetObjectCommand with jest using the v3 sdk?


Testing an s3 upload? The method to test is

export class ProcessData {
  constructor() {}

  async process(): Promise<void> {
     const data = await s3Client.send(new GetObjectCommand(bucket));
     await parseCsvData(data.Body)
}

This is my attempt at the test case.

import {S3Client} from '@aws-sdk/client-s3';
jest.mock("aws-sdk/clients/s3", () => {
  return {
    S3Client: jest.fn(() => {
        send: jest.fn().mockImplementation(() => {
            data:  Buffer.from(require("fs").readFileSync(path.resolve(__dirname, "test.csv")));
        })
    })
  }
});

describe("@aws-sdk/client-s3 mock", () => {
  test('CSV Happy path', async () => {
    const processData = new ProcessData()
    await processData.process()
  }
}

The process gets to the parse method and throws an error "The bucket you are attempting to access must be addressed using the specific endpoint"


Solution

  • For anyone who wants to mock the client directly, you can use the library aws-sdk-client-mock which is recommended by the AWS SDK team.

    Here is an introductory tutorial

    The initial steps:

    import fs from 'fs';
    
    import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
    import { mockClient } from 'aws-sdk-client-mock';
    
    const mockS3Client = mockClient(S3Client);
    

    And then you can mock it this way

    mockS3Client.on(GetObjectCommand).resolves({
      Body: fs.createReadStream('path/to/some/file.csv'),
    });
    

    You can also spy on the client

    const s3GetObjectStub = mockS3Client.commandCalls(GetObjectCommand)
    
    // s3GetObjectStub[0] here refers to the first call of GetObjectCommand
    expect(s3GetObjectStub[0].args[0].input).toEqual({
      Bucket: 'foo', 
      Key: 'path/to/file.csv'
    });
    

    Update May 6th, 2023

    Since version 3.188.0 (Oct 22), the S3 client supports util functions to consume and parse the response streams.

    So now, to mock the response, we need to wrap it with the util function sdkStreamMixin() from @aws-sdk/util-stream-node

    Update Jan 28th, 2024

    As mentioned by @Matthew Woo, the @aws-sdk/util-stream-node package is deprecated and we should use the sdkStreamMixin() function from @smithy/util-stream instead

    import fs from 'fs';
    
    import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
    import {sdkStreamMixin} from '@smithy/util-stream';
    import { mockClient } from 'aws-sdk-client-mock';
    
    const mockS3Client = mockClient(S3Client);
    const stream = sdkStreamMixin(fs.createReadStream('path/to/some/file.csv'))
    
    mockS3Client.on(GetObjectCommand).resolves({
      Body: stream ,
    });
    

    Update 18 Apr 2024

    The current docs for aws-sdk-client-mock for this are a little more complex:

    import {GetObjectCommand, S3Client} from '@aws-sdk/client-s3';
    import {sdkStreamMixin} from '@smithy/util-stream';
    import {mockClient} from 'aws-sdk-client-mock';
    import {Readable} from 'stream';
    import {createReadStream} from 'fs';
    
    const s3Mock = mockClient(S3Client);
    
    it('mocks get object', async () => {
        // create Stream from string
        const stream = new Readable();
        stream.push('hello world');
        stream.push(null); // end of stream
    
        // alternatively: create Stream from file
        // const stream = createReadStream('./test/data.txt');
    
        // wrap the Stream with SDK mixin
        const sdkStream = sdkStreamMixin(stream);
    
        s3Mock.on(GetObjectCommand).resolves({Body: sdkStream});
    
        const s3 = new S3Client({});
    
        const getObjectResult = await s3.send(new GetObjectCommand({Bucket: '', Key: ''}));
    
        const str = await getObjectResult.Body?.transformToString();
    
        expect(str).toBe('hello world');
    });