Controller tests

Let's then finish the unit tests section by creating tests for the controller.

We can go to the file users.controller.spec to begin, and also attempt to run this test file.

yarn test:watch -- users.controller

But the same error will occur, as the original service is included in the module. Therefore, it will attempt to find the Repository inside the module and will fail. We don't want the actual service here, much less the actual Repository. Only the unit that we are testing should be the focus of the test, everything else should be mocked. Let's then fix this.

Back in the testing.util file, we should create a type for a MockService and a function to instantiate one. Currently, there's nothing that uniquely identifies a service apart from it being a class, so instead let's create a type for a MockClass and the function createMockInstance().

The MockClass type will be a bit similar to MockRepository, but with a difference. Here, we should use a type parameter to indicate what will be mocked. To enforce that it should be a class, we can make the type parameter extend from the Type interface, which represents a generic constructor. There is just something we should be aware of: this type parameter will not accept the class itself, but the typeof the class. Due to this, we should give to MockProxy not the type argument itself, but a type representing an instance of this type by using InstanceType. We then get the following:

export type MockClass<T extends Type> = MockProxy<InstanceType<T>>;

And for the function to generate the instance, we'll use the same generic representing a class. We'll also have an actual regular parameter of this type, and call the mock() function passing the typeof this Class.

export const createMockInstance = <T extends Type>(Class: T) =>
  mock<typeof Class>();

Back in the file users.controller.spec, we can

  • Create a variable for the service, using typeof as was mentioned

let service: MockClass<typeof UsersService>;
  • Call the function to instantiate the desired class

useValue: createMockInstance(UsersService),
  • Obtain a reference to it from the module

service = module.get<MockClass<typeof UsersService>>(UsersService);

Let's also put those previous three auxiliary functions at the bottom of the file, once again.

Very well, now the remainder will be extremely similar to the UsersService tests. The only thing that may be a bit different is that some route handlers expect an idDto, so we should also have one there.

describe('create', () => {
  describe('when no error occurs', () => {
    it('should create a user', async () => {
      const id = 1;
      const createUserDto = genCreateDto();
      const expectedUser = genUser(id, createUserDto);
  
      service.create.mockResolvedValueOnce(expectedUser);
  
      const user = await controller.create(createUserDto);
  
      expect(user).toBe(expectedUser);
    });
  });

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

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

    service.findAll.mockResolvedValueOnce(expectedUsers);

    const users = await controller.findAll();

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

describe('findOne', () => {
  describe('when user exists', () => {
    it('should return the user', async () => {
      const id = 1;
      const idDto: IdDto = { id };
      const expectedUser = genUser(id);

      service.findOne.mockResolvedValueOnce(expectedUser);

      const user = await controller.findOne(idDto);

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

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

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

      let error: Error;

      try {
        await controller.findOne(idDto);
      } catch (err) {
        error = err;
      }

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

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

      service.update.mockResolvedValueOnce(expectedUser);

      const user = await controller.update(idDto, updateUserDto);

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

  describe('otherwise', () => {
    it('should propagate the exception', async () => {
      const id = 1;
      const idDto: IdDto = { id };
      const updateUserDto = genUpdateDto();

      const exception = new NotFoundException('User not found');
      service.update.mockRejectedValueOnce(exception);

      let error: Error;

      try {
        await controller.update(idDto, updateUserDto);
      } catch (err) {
        error = err;
      }

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

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

      service.remove.mockResolvedValueOnce(expectedUser);

      const user = await controller.remove(idDto);

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

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

      const exception = new NotFoundException('User not found');
      service.remove.mockRejectedValueOnce(exception);

      let error: Error;

      try {
        await controller.remove(idDto);
      } catch (err) {
        error = err;
      }

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

With this, we have finished the unit tests.

Commit - Implementing unit tests

Last updated