Search code examples
typescriptunit-testingjestjsamazon-dynamodb

Jest unit testing a service that injects a DynamoDB.DocumentClient in the constructor


I'm currently writing unit tests for a piece of software, and bumped into a problem that had me stumped for two days. I have services done with dependency injection pattern, which I can mock those just fine, until I got to the services that had DynamoDB.DocumentClient dependencies.

I have tried the following, using this fictitious service as example: cats.service.ts

import { DynamoDB } from 'aws-sdk';
import { Repository } from 'typeorm';
import { CatDTO, CatEntity} from 'somewhere';
export class CatsService {
    constructor(
        private readonly catRepository: Repository<CatEntity>,
        private readonly clientDB: DynamoDB.DocumentClient,
){}

async findCat (name: string): Promise<CatDTO> {
    try{    
const result = await this.clientDB.query(name).promise();
    return result
} catch(error){
  throw new Error(error) 
}

The problem: I can mock the repositories, but not the AWS SDK DynamoDB.DocumentClient. I have tried lots of stuff, such as jest's manual mocking of the module, jest.mock ing directly on the code as described here, here, here and also here.

The thing is: I tried going for something similar that I do with the typeorm repositories, that is:

const mockCatRepository = {
    findOne: jest.fn(),
} as unknown as Repository<CatEntity>;

// Here I tried mocking the clientDB DocumentClient in all the ways linked above, such as:
jest.mock('aws-sdk', () => {
  return {
    DynamoDB: {
      DocumentClient: jest.fn(),
    },
  };
});

describe('CatsService typeorm repository dependency example', () => {
    let service: CatsService;
    let mockClientDB: DynamoDB.DocumentClient;
    beforeEach(() => {
        mockClientDB = {} as DynamoDB.DocumentClient;
        mockClientDB.query = jest.fn().mockReturnValue({ promise: jest.fn() });
        service = new CatsService(
            mockCatRepository,
            mockClientDB,
        );
    });


it('should return a cat successfully', async () => {
    const mockInput='Peanut';
// and nothing I tried above I could manage to do something like:
    const mockDynamoReturn = { Items: [{ name: 'Peanut', breed: 'Tabby' }] };
    clientDB.query.promise.mockResolvedValueOnce(mockDynamoReturn)
// since either query or promise don't actually get mocked.
// in this specific case, the following error: 

error TS2339: Property 'mockResolvedValue' does not exist on type '() => Promise<PromiseResult<QueryOutput, AWSError>>'

Thanks for your time reading this!


Solution

  • Since you can easily create mock objects for clientDB and catRepository, and pass them to CatsService's constructor, you don't need to use jest.mock() to mock the entire aws-sdk module.

    E.g.

    export class CatsService {
        constructor(private readonly catRepository, private readonly clientDB) {}
    
        async findCat(name) {
            try {
                const result = await this.clientDB.query(name).promise();
                return result;
            } catch (error) {
                throw new Error(error);
            }
        }
    }
    
    import { CatsService } from '.';
    
    describe('76675025', () => {
        let service;
        let mockClientDB;
        const mockCatRepository = {
            findOne: jest.fn(),
        };
    
        beforeEach(() => {
            mockClientDB = {
                query: jest.fn().mockReturnThis(),
                promise: jest.fn(),
            };
            service = new CatsService(mockCatRepository, mockClientDB);
        });
    
        test('should pass', async () => {
            const mockInput = 'Peanut';
            const mockDynamoReturn = { Items: [{ name: 'Peanut', breed: 'Tabby' }] };
            mockClientDB.promise.mockResolvedValueOnce(mockDynamoReturn);
            await service.findCat(mockInput);
            expect(mockClientDB.query).toBeCalledWith('Peanut');
            expect(mockClientDB.promise).toBeCalledTimes(1);
        });
    });