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. Currently, there's nothing that uniquely identifies a service as a service, apart from it being a class, so instead let's create a type for a MockClass.
The MockClass type will be a bit similar to MockRepository, but with a difference. Here, we should use a type parameter to indicate that a class will be mocked. To enforce that, we can make it 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, which represents its constructor. Due to this, we should give to MockProxy not the type argument itself, as it represents the constructor, but the type of its instance, by using InstanceType. To enforce a class, unfortunately this juggling is necessary. We then get the following:
export type MockClass<TClass extends Type> = MockProxy<InstanceType<TClass>>;
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 base mock
useValue: createMock(),
Obtain a reference to it from the module
service = module.get(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);
});
});
});