Search code examples
unit-testingpromisejestjsnestjsts-jest

How to mock a promise rejection with Jest


What am I doing?

I am following a course on nestjs which has some unit testing it it. I wrote this test that checks the signUp method in an repository class. The problem is that in order to trigger the exceptions the line user.save() should return a promise rejection (simulating some problem writing to db). I tried a few ways (see below) but none that work.

The problem

The result is that the test succeeds, but there is an unhandled Promise rejection. This way even if I assert that is does not.toThow() it will succeed with the same unhandled Promise rejection

(node:10149) UnhandledPromiseRejectionWarning: Error: expect(received).rejects.toThrow()

Received promise resolved instead of rejected
Resolved to value: undefined
(Use `node --trace-warnings ...` to show where the warning was created)

How do I make it reject the promise correctly?

The code

Below is the code of my test and the function under test.

import { ConflictException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { AuthCredentialsDto } from './dto/auth-credentials.dto';
import { UserRepository } from './user.repository';

describe('UserRepository', () => {
  let userRepository: UserRepository;

  let authCredentialsDto: AuthCredentialsDto = {
    username: 'usahh',
    password: 'passworD12!@',
  };

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [UserRepository],
    }).compile();

    userRepository = module.get<UserRepository>(UserRepository);
  });

  describe('signUp', () => {
    let save: any;
    beforeEach(() => {
      save = jest.fn();
      userRepository.create = jest.fn().mockReturnValue({ save });
    });

    it('throws a conflict exception if user already exist', () => {
      // My first try:
      // save.mockRejectedValue({
      //   code: '23505',
      // });

      // Then I tried this, with and without async await:
      save.mockImplementation(async () => {
        await Promise.reject({ code: '23505' });
      });
      expect(userRepository.signUp(authCredentialsDto)).rejects.toThrow(
        ConflictException,
      );
    });
  });
});


The function under test here is:

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async signUp(authCredentialsDto: AuthCredentialsDto): Promise<void> {
    const { username, password } = authCredentialsDto;
    const user = this.create();

    user.salt = await bcrypt.genSalt();
    user.username = username;
    user.password = await this.hashPassword(password, user.salt);

    try {
      await user.save();
    } catch (e) {
      if (e.code === '23505') {
        throw new ConflictException('Username already exists');
      } else {
        throw new InternalServerErrorException();
      }
    }
  }
}

Solution

  • This should be asynchronous test but it's synchronous, it won't fail even if there's rejected promise.

    A promise that expect(...).rejects... returns needs to be chained:

    it('throws a conflict exception if user already exist', async () => {
      ...
      await expect(userRepository.signUp(authCredentialsDto)).rejects.toThrow(
        ConflictException,
      );
    });
    

    There's no room for trial and error for mockImplementation. A mock is supposed to return rejected promise, and mockRejectedValue does this. mockImplementation(async () => ...) is unnecessarily long way to write it.