20

I am tryng to code a test for upload. But i am not understating how to properly use jest.mock('aws-sdk')

export class S3Service {
  private readonly s3: S3;
  private readonly bucket: string;
  constructor(private readonly configService: ConfigService) {
    this.s3 = new S3({
      accessKeyId: this.configService.get(''),
      secretAccessKey: this.configService.get(''),
      region: this.configService.get(''),
    });
    this.bucket = this.configService.get('');
  }
async upload(name: string, contentType: string, buffer: Buffer): Promise<string> {
    const upload = await this.s3.upload({params...}).promise();
    return upload;
  }
}
Lin Du
  • 88,126
  • 95
  • 281
  • 483
luickx
  • 209
  • 1
  • 2
  • 5

5 Answers5

33

Here is the unit test solution:

s3Service.ts:

import { S3 } from 'aws-sdk';

export class S3Service {
  private readonly s3: S3;
  private readonly bucket: string;
  constructor(private readonly configService) {
    this.s3 = new S3({
      accessKeyId: this.configService.get(''),
      secretAccessKey: this.configService.get(''),
      region: this.configService.get(''),
    });
    this.bucket = this.configService.get('');
  }
  public async upload(name: string, contentType: string, buffer: Buffer): Promise<any> {
    const bucket = this.bucket;
    const params = { Bucket: bucket, Key: 'key', Body: buffer };
    const upload = await this.s3.upload(params).promise();
    return upload;
  }
}

s3Service.test.ts:

import { S3Service } from './s3Service';

const mS3Instance = {
  upload: jest.fn().mockReturnThis(),
  promise: jest.fn(),
};

jest.mock('aws-sdk', () => {
  return { S3: jest.fn(() => mS3Instance) };
});

describe('61830632', () => {
  it('should upload correctly', async () => {
    const configService = {
      get: jest
        .fn()
        .mockReturnValueOnce('accessKeyId')
        .mockReturnValueOnce('secretAccessKey')
        .mockReturnValueOnce('us-east')
        .mockReturnValueOnce('bucket-dev'),
    };
    mS3Instance.promise.mockResolvedValueOnce('fake response');
    const s3Service = new S3Service(configService);
    const actual = await s3Service.upload('name', 'contentType', Buffer.from('ok'));
    expect(actual).toEqual('fake response');
    expect(mS3Instance.upload).toBeCalledWith({ Bucket: 'bucket-dev', Key: 'key', Body: Buffer.from('ok') });
  });
});

unit test results with 100% coverage:

 PASS  stackoverflow/61830632/s3Service.test.ts (11.362s)
  61830632
    ✓ should upload correctly (6ms)

--------------|---------|----------|---------|---------|-------------------
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------|---------|----------|---------|---------|-------------------
All files     |     100 |      100 |     100 |     100 |                   
 s3Service.ts |     100 |      100 |     100 |     100 |                   
--------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        12.738s
Lin Du
  • 88,126
  • 95
  • 281
  • 483
  • Thanks for the illustrative answer. Conceptually, what does ```jest.fn().mockReturnThis()``` do? Does it let you chain calls? – cischa Nov 17 '20 at 05:49
  • 1
    @cischa Yes. It let you chain calls. – Lin Du Nov 17 '20 at 05:52
  • 2
    @slideshowp2 Copying your code in my test returns this error from jest: ReferenceError: .../__tests__/user_handler.spec.ts: The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables. Invalid variable access: mS3Instance Allowed objects: Array, ArrayBuffer, Atomics, BigInt, BigInt64Array, BigUint64Array, Boolean, Buffer, DTRACE_HTTP_CLIENT_REQUEST, DTRACE_HTTP_CLIENT_RESPONSE, DTRACE_HTTP_SERVER_REQUEST, DTRACE_HTTP_SERVER_RESPONSE, DTRACE_NET_SERVER_CONNECTION, DTRACE_NET_STREAM_END, DataView, Date, Error, EvalError,..... – Benjamin Heinke Jan 21 '21 at 22:27
  • @BenjaminHeinke This is just a warning. Rename `mS3Instance` to something that begins with `mock` - for example `mockS3`. – timomeinen Feb 15 '21 at 20:32
  • 1
    I tried this `const mockS3Instance = { putObject: jest.fn(), } console.log(mockS3Instance) jest.mock('aws-sdk', () => { return { S3: jest.fn(() => mockS3Instance) } });` but I am getting error like `ReferenceError: Cannot access 'mockS3Instance' before initialization` on this line 'return { S3: jest.fn(() => mockS3Instance) }' – Abhilash D K Jan 12 '23 at 22:12
7

Here is my solution:

jest.mock('aws-sdk', () => {
  class mockS3 {
    getSignedUrl(op, obj) {
      return 'url';
    }
  }
  return {
    ...jest.requireActual('aws-sdk'),
    S3: mockS3
  };
});
mikemaccana
  • 110,530
  • 99
  • 389
  • 494
akash ingole
  • 151
  • 1
  • 2
3

If you are using NestJS, it can be easier than you think.

File Upload Module:

import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { S3 } from 'aws-sdk';
import { FileUploadController } from './file-upload.controller';
import { FileUploadRepository } from './file-upload.repository';
import { FileUploadService } from './file-upload.service';
@Module({
  imports: [TypeOrmModule.forFeature([FileUploadRepository])],
  controllers: [FileUploadController],
  providers: [
    FileUploadService,
    {
      provide: S3,
      useFactory: (configService: ConfigService) =>
        new S3({
          accessKeyId: configService.get('AWS_ACCESS_KEY'),
          secretAccessKey: configService.get('AWS_ACCESS_SECRET'),
          region: configService.get('AWS_REGION'),
        }),
    },
  ],
})
export class FileUploadModule {}

The service itself:

import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { S3 } from 'aws-sdk';
import { v4 as uuid } from 'uuid';
import { FileUpload } from './file-upload.entity';
import { FileUploadRepository } from './file-upload.repository';

@Injectable()
export class FileUploadService {
  constructor(
    @InjectRepository(FileUploadRepository)
    private readonly publicFileRepository: FileUploadRepository,
    private readonly configService: ConfigService,
    private readonly s3: S3,
  ) {}

  async uploadPublicFile(
    dataBuffer: Buffer,
    filename: string,
  ): Promise<FileUpload> {
    const uploadResult = await this.s3
      .upload({
        Bucket: this.configService.get('AWS_BUCKET_NAME'),
        Body: dataBuffer,
        Key: `${uuid()}-${filename}`,
      })
      .promise();

    const createdFile = this.publicFileRepository.create({
      key: uploadResult.Key,
      url: uploadResult.Location,
    });
    await this.publicFileRepository.save(createdFile);
    return createdFile;
  }

  async deletePublicFile(
    publicFileId: string,
    publicFileKey: string,
  ): Promise<FileUpload> {
    const response = await this.s3
      .deleteObject({
        Bucket: this.configService.get('AWS_BUCKET_NAME'),
        Key: publicFileKey,
      })
      .promise();
    if (!response) {
      throw new InternalServerErrorException(
        `Could not delete file ${publicFileKey}`,
      );
    }
    const { raw: deletedItem } = await this.publicFileRepository.delete(
      publicFileId,
    );
    return deletedItem;
  }
}

and then finally the test:

import { InternalServerErrorException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { S3 } from 'aws-sdk';
import { mockFileUpload } from './file-upload.mock';
import { FileUploadRepository } from './file-upload.repository';
import { FileUploadService } from './file-upload.service';

export const mockFileUploadRepository = () => ({
  create: jest.fn(),
  save: jest.fn(),
  delete: jest.fn(),
});

const mS3Instance = {
  upload: jest.fn().mockReturnThis(),
  promise: jest.fn(),
  deleteObject: jest.fn().mockReturnThis(),
};

describe('FileUploadService', () => {
  let service: FileUploadService;
  let repository;
  let s3Service;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        FileUploadService,
        {
          provide: FileUploadRepository,
          useFactory: mockFileUploadRepository,
        },
        {
          provide: ConfigService,
          useValue: {
            get: jest.fn(),
          },
        },
        {
          provide: S3,
          useFactory: () => mS3Instance,
        },
      ],
    }).compile();

    service = module.get<FileUploadService>(FileUploadService);
    repository = module.get<FileUploadRepository>(FileUploadRepository);
    repository = module.get<FileUploadRepository>(FileUploadRepository);
    s3Service = module.get<S3>(S3);
  });

  describe('FileUploadService.uploadPublicFile', () => {
    it('should create public file and throw no error', async () => {
      repository.create.mockResolvedValue(mockFileUpload);
      repository.save.mockResolvedValue(mockFileUpload);

      s3Service.promise = jest.fn().mockResolvedValue({
        Key: 'some-key',
        Location: 'some-location',
      });

      const file = Buffer.alloc(513, '0');
      const response = await service.uploadPublicFile(file, 'somefilename');
      expect(response).toBeDefined();
    });
  });

  describe('FileUploadService.deletePublicFile', () => {
    it('should delete public file and throw no error', async () => {
      repository.delete.mockResolvedValue({ raw: mockFileUpload });
      repository.save.mockResolvedValue(mockFileUpload);

      s3Service.promise = jest.fn().mockResolvedValue({
        Key: 'some-key',
        Location: 'some-location',
      });

      const response = await service.deletePublicFile('someid', 'somefilename');
      expect(response).toBeDefined();
    });

    it('should not delete public file and throw InternalServerErrorException', async () => {
      repository.delete.mockResolvedValue({ raw: mockFileUpload });
      repository.save.mockResolvedValue(mockFileUpload);

      s3Service.promise = jest.fn().mockResolvedValue(null);

      const promise = service.deletePublicFile('someid', 'somefilename');
      expect(promise).rejects.toThrow(InternalServerErrorException);
    });
  });
});

Lucas P
  • 51
  • 3
  • Thanks for this. I can confirm that the above works. – Elte156 Feb 03 '22 at 03:02
  • Man that was a life saver thank you very much. I've worked with NestJS services in my other tests but for some reason i went full retard when dealing with S3, if only i had realized sooner that there was an easier way. – Kaluk Jun 14 '22 at 16:42
  • Thank you! I had to make one small change and put `S3` in double quotes (`provide: "S3"`), because I am injecting it in the service like this: `@Inject("S3") private readonly s3: S3,` – Tobias May 12 '23 at 11:58
0

This would be basic approach to test the S3 aws-sdk.Lets say we have delete method for S3 in the AWS Lambda

DeleteS3Object.js


const AWS = require('aws-sdk');


const DeleteS3Object = (testBucketParams) => {
  const s3 = new AWS.S3();

  try {
    const s3DeleteResponse = await s3.deleteObject(params).promise();
    debugger;
    console.log(`Delete S3 Object successfully:`, s3DeleteResponse);
    resolve(s3DeleteResponse);
  } catch (error) {
    console.log(`Delete S3 Object failed:`, JSON.stringify(error));
    reject(error);
  }

};

exports.handler = deleteS3Object;

DeleteS3Object.test.js



const AWSMock = require('aws-sdk-mock');
const AWS = require('aws-sdk');
const DeleteS3Object = require('./DeleteS3Object').handler;

describe('Test Delete S3 object', () => {
  beforeEach(() => {
    AWSMock.setSDKInstance(AWS);
  });

  afterEach(() => {
    AWSMock.restore('S3');
  });

  it('Should be successfully completed', async () => {
    const successResult = { success: true };
    AWSMock.mock('S3', 'deleteObject', (params, callback) => {
      expect(params).toEqual({ Bucket: 'test', Key: 'test' });
      return callback(null, successResult);
    });

    const paramsTest = { Bucket: 'test', Key: 'test' };

    const finalResponse = await DeleteS3Object(paramsTest);
    expect(finalResponse).toBe(successResult);
  });


  it('Should fail', async () => {
    const failResult = { success: false };
    AWSMock.mock('S3', 'deleteObject', (params, callback) => {
      expect(params).toEqual({ Bucket: 'test', Key: 'test' });
      return callback(failResult);
    });
    const paramsTest = {
      basicS3ParamList: { Bucket: 'test', Key: 'test' },
      timeToDelete: 1000
    };
    try {
      await DeleteS3Object(paramsTest);
    } catch (error) {
      expect(error).toEqual(failResult);
    }
  });
})


Siddharth Sunchu
  • 866
  • 10
  • 13
0

For reference, if you happen to be importing the specific service instead of the full SDK, eg

import S3 from 'aws-sdk/clients/s3';

you would change the mocking factory to instead be

const mS3Instance = {
  upload: jest.fn().mockReturnThis(),
  promise: jest.fn(),
};

jest.mock('aws-sdk/clients/s3', () => {
  return jest.fn(() => mockS3Instance);
});

Nick Hammond
  • 193
  • 13