Remaining service tests

We already have a structure to easily implement the remaining tests.

Inside the describe() for the findOne() method, we may then have another describe() below the previous one for when a user is not found. What we'll do:

  • Have an exception representing this situation

  • Make the repository method return a rejected promise with this exception

  • Check if the exception was propagated

describe('otherwise', () => {
  it('should propagate the exception', async () => {
    const id = 1;

    const exception = new NotFoundException('User not found');
    repository.findOneByOrFail.mockRejectedValueOnce(exception);

    let error: Error;

    try {
      await service.findOne(id);
    } catch (err) {
      error = err;
    }

    expect(error).toBe(exception);
  });
});

Alright, now for findAll(). This one will be quite simple, we

  • Create an array with expectedUsers

  • Make the find() method of the repository return them

  • Check if the obtained users match the expectedUsers

describe('findAll', () => {
  it('should return an array of users', async () => {
    const expectedUsers = [genUser(1), genUser(2)];

    repository.find.mockResolvedValueOnce(expectedUsers);

    const users = await service.findAll();

    expect(users).toBe(expectedUsers);
  });
});

For create(), let's

  • Have a fake createUserDto and an expectedUser with the contents of this DTO

  • Make the create() and save() methods of the repository return them, respectively

  • Check if the result matches the expectation.

describe('when no errors occur', () => {
  it('should create a user', async () => {
    const id = 1;
    const createUserDto = genCreateDto();
    const expectedUser = genUser(id, createUserDto);
  
    repository.create.mockReturnValueOnce(createUserDto);
    repository.save.mockResolvedValueOnce(expectedUser);
  
    const user = await service.create(createUserDto);
  
    expect(user).toBe(expectedUser);
  });
});

It should also propagate exceptions, so let's test this too.

describe('otherwise', () => {
  it('should propagate exceptions', async () => {
    const createUserDto = genCreateDto();
  
    const exception = new ConflictException('Error creating user');
    repository.create.mockReturnValueOnce(createUserDto);
    repository.save.mockRejectedValueOnce(exception);
  
    let error: Error;
  
    try {
      await service.create(createUserDto);
    } catch (err) {
      error = err;
    }
  
    expect(error).toBe(exception);
  });
});

For update(), we

  • Have an existingUser, a fake updateUserDto and an expectedUser, which receives the contents of the existingUser and then of the updateUserDto over them

  • Have preload() and save() return the expectedUsed

  • Perform the assertion as usual

describe('when user exists', () => {
  it('should update the user', async () => {
    const id = 1;
    const existingUser = genUser(id);
    const updateUserDto = genUpdateDto();
    const expectedUser = {
      ...existingUser,
      ...updateUserDto,
    } as User;

    repository.preload.mockResolvedValueOnce(expectedUser);
    repository.save.mockResolvedValueOnce(expectedUser);

    const user = await service.update(id, updateUserDto);

    expect(user).toBe(expectedUser);
  });
});

And also check in the case of an exception. Remember that, in this case, it's the service itself that throws it and not the repository. There is no need to mock the return of the repository methods here because, by default, they don't return anything. So, when preload() returns undefined, the exception will be thrown, similarly to when the user is not found.

describe('otherwise', () => {
  it('should throw the adequate exception', async () => {
    const id = 1;
    const updateUserDto = genUpdateDto();

    const exception = new NotFoundException('User not found');

    let error: Error;

    try {
      await service.update(id, updateUserDto);
    } catch (err) {
      error = err;
    }

    expect(error).toEqual(exception);
  });
});

Here, toEqual() is used instead of toBe() because, remember that the exception is a composite type. With primitive types only the value matters, but for composite types the reference is also important. So, as in this case it's not this exact exception that will be thrown, but another one that is identical to it, we need to use toEqual() to check if our exception here has the same shape of the one being thrown, whereas using toBe() would check if they are the exact same exception, which is not the case here.

In the case of remove(), we have an interesting difference. Here, we call the findOne() method of the service itself. It would be interesting to mock it here as we don't want to have its actual functionality, as it is not the focus of this test. But how could we achieve this, as it is a method from the actual service and not from a mock? This can be done with the spyOn() function from Jest. It allows for applying a mocked implementation to a "normal" method through the following form:

describe('when user exists', () => {
  it('should remove the user', async () => {
    const id = 1;
    const expectedUser = genUser(id);

    jest.spyOn(service, 'findOne').mockResolvedValueOnce(expectedUser);
    repository.remove.mockResolvedValueOnce(expectedUser);

    const user = await service.remove(id);

    expect(user).toBe(expectedUser);
  });
});

We can do the same in the case of an exception.

describe('otherwise', () => {
  it('should propagate the exception', async () => {
    const id = 1;

    const exception = new NotFoundException('User not found');
    jest.spyOn(service, 'findOne').mockRejectedValueOnce(exception);

    let error: Error;

    try {
      await service.remove(id);
    } catch (err) {
      error = err;
    }

    expect(error).toBe(exception);
  });
});

And we're done! Let's then proceed to the tests for the UsersController. They will be quite simple.

Last updated