Some theory & First test

A bit of theory before proceeding further.

Now, let's begin to write the unit tests for the UsersService. Inside the file users.service.spec, we may then continue our discussion.

Basically, these test files are organized in two main kinds of blocks:

  • describe() - describe elements or scenarios and group closely-related tests together

  • it() - short for individual test, the test itself

The it() name also makes the test more readable: it should do something.

Therefore, as we are testing the methods of the UsersService, its natural that we have the following organization:

  • First, everything is put inside a describe() block for the UsersService itself

  • Then, inside it, we should have a describe() for each one of its methods

  • After that, inside each one, we can have a describe() for each possible scenario

  • Finally, inside them, write the it() block or the test itself

Very well, but notice the beforeEach() method. Inside it, logic is executed before each test. Alongside it, there are three other methods that execute at distinct moments:

  • beforeAll() - before all the tests, once

  • beforeEach() - before each test

  • afterEach() - after each test

  • afterAll() - after all the tests, once

So, what happens here is that, before each test, a module of type TestingModule will be initialized with just the UsersService. After that, a variable for the service obtains its reference from within the module. It's done like this because unit tests should be performed in isolation, so we should test only the UsersService. Any external dependencies should be mocked, that is, have a simplified representation. We will not test the Repository from TypeORM, we should assume that it works properly because it's not our responsibility to test it. Our focus should be only to test our own code.

Due to this, before each test we have a fresh service. The first test, which comes by default, is the it should be defined test. It simply expects the service to be defined. The expect() function receives an argument which we expect something from, and can be chained with many useful functions to perform different assertions, such as toBeDefined() in this case.

It will obviously pass, right? Actually, it will fail. This is because the UsersService relies on the Repository, which is not included in the module. But we should not use the actual Repository, nor connect to a database. As was said, this breaks that isolation principle mentioned earlier. So, at least for now, let's have the repository as an empty object, in the providers of the module. To represent a Repository here, we can use the getRepositoryToken() function, passing the entity as argument.

provide: getRepositoryToken(User),
useValue: {},

By executing the test again, now it passes! Let's then finally begin our first test, for the findOne() method. This method receives an id, so we'll perform these steps for now:

  • Create a fake id and an expectedUser

  • Call the method in the service

  • Check if the obtained user matches the expectedUser

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

      const user = await service.findOne(id);

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

This will, however, not work because the repository is an empty object. The service will attempt to invoke a non-existing method in the repository, causing the test to fail. How could we solve this? Well, the repository should have its methods as mocked functions. They represent functions without any actual logic, but this implementation can be mocked for each specific test. Let's then learn how to have a mocked repository.

Last updated