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() - describes elements or scenarios and groups closely-related tests together

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

The it() name also makes the test name more readable, for example: 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

  • beforeEach() - before each test

  • afterEach() - after each test

  • afterAll() - after all the tests

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 and checks whether it meets certain conditions. It 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! We're about to begin our first test, but first, let's just see some more fundamental theory.

Tests ideally should follow the AAA pattern, which means:

  • Arrange: Set up the data necessary to perform both the test and the assertions

  • Act: Peform the test itself, using an actual element of the system

  • Assert: Verify if the output of the test is what it should be

Let's then write our first test, for the findOne() method. It receives an id and returns a user, so we'll perform these steps for now, following the AAA pattern:

  • 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