Maciej Cieślar A JavaScript developer and a blogger @ https://www.mcieslar.com/

Unit testing NestJS applications with Jest

12 min read 3466

Unit-testing NestJS Applications With Jest

Testing applications can seem like a fairly complicated concept, and thus, many programmers avoid it due to the fear of failure — especially in the Node.js world, where testing applications are not so ubiquitous as in, say, Java, and the resources on testing are scarce.

Although getting an application tested thoroughly is a complicated thing to do, unit testing is the easiest kind of testing to grasp. As its name suggests, unit testing focuses on writing tests for the smallest possible units. A unit is often a single function or method. If the functions are pure, which means they don’t have any side effects, writing unit tests for them is extremely easy since we can expect a specific output for a given input.

expect(add(2, 2)).toBe(4);

Now, we can’t really assume that every function in our codebase is going to be pure. So the tests will get more complicated than simply calling a function and seeing whether the output is what we expect.

Usually, our functions internally make use of other functions. For example, there might be a method called userService.createUser(), which would internally call userRepository.create() to create the user entity and userRepository.save() to save it in the database of choice. In this case, testing the output would be silly at best and wouldn’t really give us any confidence in whether the code is working as expected.

Unit tests are only interested in the logic that the function itself contains, not the external one.

So in this example, the test would check whether userRepository.create() has been called and, if so, with what arguments. Isolating the logic from all the external dependencies is called mocking. It means replacing all the specific implementations with fake ones that are monitored by the testing environment. This way, tracking the calls that have been made to a certain external method or overriding its return value is a breeze.

What makes unit testing unique

The thing about unit tests is that they shouldn’t be dependent on the environment in which they are being run, and they are supposed to be fast. Just to give you an example of the magnitude of speed you’d expect here: unit tests should be run after each commit. Of course, there are many techniques, usually based on Git, that allow us to run only the tests that have been themselves changed or that depend on the files that have been changed.

End-to-end (E2E) tests would be an example of those that are dependent on their environment. In E2E tests, we test an application as a whole, so we provide a test database and prepare the expected environment. But it is slow and needs a special setup, and thus, it is not really feasible to be run during development, which is when we usually run unit tests. There should be a specialized setup on a CI/CD service that would take care of running E2E tests.

Controlling external dependencies has one very important downside, though. In the test, we override the value that would normally be returned by the dependency. If, for whatever reason, the external dependency changes and now returns a { token, user } object instead of a User object, the tests would still pass because it would still return a User object in our test environment.

That is why unit testing is only one of many kinds of tests. It ensures that the function has the expected behavior when we control all the external dependencies. There are also, for example, integration tests and contract tests that can assure us the contracts (expected input/output values of the external dependencies) haven’t changed.

We made a custom demo for .
No really. Click here to check it out.

But let’s be honest, it’s not like we change our methods every other day. Most of the code never changes, or at least the I/O values don’t, so by starting with unit tests only, we can still have much better confidence in the code that we write.

How NestJS helps us write unit tests

Dependency injection

Nest forces us to write more easily testable code through its built-in dependency injection — a design pattern that states that a central authority is taking care of creating and supplying dependencies, while the classes simply assume that the dependency will be provided instead of creating it themselves. So, instead of writing:

constructor() {
  this.a = new A();
}

We would write:

constructor(private readonly a: A) {}

The latter syntax is possible due to the JS Metadata Reflection API that was proposed not so long ago.

Dependency injection is usually based on interfaces rather than concrete classes. In TypeScript, however, interfaces (and types in general) are only available during compile time rather than runtime, and thus can’t be relied upon afterwards. So instead of using interfaces, in Nest it is common to use class-based injection.

The testing module

With dependency injection, replacing our dependencies becomes trivial. But with unit tests, we would rather not recreate the whole application for each test. Instead, we would like to create the bare minimum setup. Thankfully, Nest provides us with the @nestjs/testing package. The package enables us to create a Nest module, like we normally would, by declaring only the dependencies used in the tests. Here’s an example:

const module: TestingModule = await Test.createTestingModule({
  providers: [
    PlaylistService,
    {
      provide: getRepositoryToken(Playlist),
      useClass: PlaylistRepositoryFake,
    },
  ],
}).compile();

playlistService = module.get(PlaylistService);
playlistRepository = module.get(getRepositoryToken(Playlist));

So in the example above, we “import” to the testing module two things: playlistService and playlistRepository. This setup would be ideal to test things like creating a playlist; we would check whether the repository is called with the expected parameters.

Hopefully, by now you understand why Nest’s codebases are easily testable and what’s going on behind the scenes. Now let’s take a look at a practical example: testing the aforementioned playlist module scenario.

The real-world example

The repository contains the code that will be used later on in the examples. Bear in mind that this is a very basic setup that doesn’t necessarily reflect how an application should be structured.

We are going to focus on the PlaylistService class that is responsible for creating, finding, updating, and removing playlists. In a more sophisticated setup, we would probably create separate classes for each case (one for creating, one for updating) and their related logic, but we are going to keep it simple.

@Injectable()
export class PlaylistService {
  public constructor(
    @InjectRepository(Playlist)
    private readonly playlistRepository: Repository<Playlist>,
  ) {}

  public async findOneByIdOrThrow(id: string): Promise<Playlist> {
    const playlist = await this.playlistRepository.findOne({
      id,
    });

    if (!playlist) {
      throw new NotFoundException('No playlist found.');
    }

    return playlist;
  }

  public async createOne(
    createPlaylistData: CreatePlaylistData,
  ): Promise<Playlist> {
    const { title, description } = createPlaylistData;

    const playlist = this.playlistRepository.create({
      title,
      description,
    });

    const createdPlaylist = await this.playlistRepository.save(playlist);

    return createdPlaylist;
  }

  public async removeOne(
    removePlaylistData: RemovePlaylistData,
  ): Promise<void> {
    const { id } = removePlaylistData;

    const playlist = await this.findOneByIdOrThrow(id);

    await this.playlistRepository.remove([play­list]);

    return null;
  }

  public async updateOne(
    updatePlaylistData: UpdatePlaylistData,
  ): Promise<Playlist> {
    const { id, ...updateData } = updatePlaylistData;

    const existingPlaylist = await this.findOneByIdOrThrow(id);

    const playlist = this.playlistRepository.create({
      ...existingPlaylist,
      ...updateData,
    });

    const updatedPlaylist = await this.playlistRepository.save(playlist);

    return updatedPlaylist;
  }
}

All of the methods are the most basic implementation of the CRUD behavior. Essentially, they are calling their respective repository methods, so the .createOne() method calls playlistRepository.save(), the .findOne() method calls playlistRepository.findOne(), etc.

With the implementation in place, let’s test it.

Writing tests

By default, the projects generated with the Nest CLI are set up to run tests with Jest, so we are going to stick with it.

Writing unit tests is easy once you get the hang of it. It is really just creating scenarios in which the method would behave differently and then testing its expected behavior.

Before writing the tests, let’s create the testing module.

describe('PlaylistService', () => {
  let playlistService: PlaylistService;
  let playlistRepository: Repository<Playlist>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PlaylistService,
        {
          provide: getRepositoryToken(Playlist),
          useClass: PlaylistRepositoryFake,
        },
      ],
    }).compile();

    playlistService = module.get(PlaylistService);
    playlistRepository = module.get(getRepositoryToken(Playlist));
  });
});

Just as in the example above, the module will create the playlistService and playlistRepository and resolve their dependencies. Since we don’t want to create an actual database connection, we are overriding the playlistRepository with PlaylistRepositoryFake. The getRepositoryToken function lets us get the injection token of the repository.

Providing a fake

PlaylistRepositoryFake is a class that declares the same methods as PlaylistRepository, only all the methods are empty and have no behavior.

export class PlaylistRepositoryFake {
  public create(): void {}
  public async save(): Promise<void> {}
  public async remove(): Promise<void> {}
  public async findOne(): Promise<void> {}
}

You might be wondering why we have to do that — won’t we be mocking all the external dependencies?

The fakes have two important roles:

1. Expecting certain conditions to be met during the creation of a class instance

The first reason to use fakes is especially important in our example: a class, playlistRepository, that has some specific logic executed during the creation of an instance.

The repository, when created, expects the database module to be imported (in the module context) and the connection to the database to be open. Since we are not importing the database module and there is no database running at all, the repository would throw an error during creation of the module.

By using a whole other class (the fake), all the logic related to the database module is gone. It is a simple object that does nothing.

2. Monitoring the calls to the external dependencies

In order to check that a method actually made use of an external dependency, we have to somehow register that the call has been made. For example, as discussed before, we would like to be sure that the .createOne() method actually called playlistRepository.save() and saved the entity in the database.

In the testing module setup, we save the references to each created instance, so playlistRepository and playlistService each have references to the objects instantiated by the testing module.

playlistService = module.get(PlaylistService);
playlistRepository = module.get(getRepositoryToken(Playlist));

Since objects in JavaScript are passed around by reference, we can easily override a specific method, like:

let calledTimes = 0

playlistRepository.save = () => {
  calledTimes += 1;
}

But doing so by hand would be tiresome and error-prone. Jest provides us with a better way to do that: spies.

Spies are defined in the following manner:

const playlistRepositorySaveSpy = jest
  .spyOn(playlistRepository, 'save')
  .mockResolvedValue(savedPlaylist);

This spy does two things: it overrides both the .save() method of playlistRepository and provides an API for developers to choose what should be returned instead. In this snippet, we use .mockResolvedValue() as opposed to .mockReturnValue() due to the original implementation returning a promise.

By keeping a reference to the spy (assigning it to a variable), we can later on, in the test, do the following:

expect(playlistRepositorySaveSpy).toBeCalledWith(createdPlaylistEntity);

Note that the variable name consists of three parts: first, the name of the object that is being overriden; second, the method’s name; and third, the word Spy, so we can easily distinguish it from our other variables. Keeping the variables’ names structured like that helped me personally to keep my tests cleaner and more readable overall.

Although we are overriding the behavior of a method, Jest’s spies still require the provided object to have said property. So if we provided a simple {} empty object, Jest would throw the following error:

Cannot spy the updateOne property because it is not a function; undefined given instead

Fakes, stubs, and test doubles

I have decided to name the replacement class as “Fake” because, to the best of my knowledge, that is the appropriate name for an object that contains the simplified logic of a class, or a total lack thereof.

People often get confused as to the difference between a fake and a test double, and all the other names thrown around in the testing world. Here’s a great article explaining the different names used in tests and their meanings.

Note that in Jest, spies are mocks and can also be stubs since they are registering the calls (mock) and can override the returned value (stub).

With all the jargon out of the way, let’s write some tests.

Testing the .createOne() method

Let’s quickly take a look at the .createOne() method implementation:

public async createOne(
  createPlaylistData: CreatePlaylistData,
): Promise<Playlist> {
  const { title } = createPlaylistData;

  if (!title) {
    throw new BadRequestException('Title is required.');
  }

  const playlist = this.playlistRepository.create({
    title,
  });

  const createdPlaylist = await this.playlistRepository.save(playlist);

  return createdPlaylist;
}

As a parameter, the method takes an object that holds the title of the playlist. If the title isn’t there, the method throws an error. That’s the first case we’ll test for.

describe('creating a playlist', () => {
  it('throws an error when no title is provided', async () => {
    expect.assertions(2);

    try {
      await playlistService.createOne({ title: '' });
    } catch (e) {
      expect(e).toBeInstanceOf(BadRequestException);
      expect(e.message).toBe('Title is required.');
    }
  });
});

I like to group tests in logical blocks that describe the action taking place — so, in this case, creating a playlist.

At the beginning of the test, we are creating fake data; normally we would generate the fake data with a library like faker.js, but here, the only thing we need is an empty string. Since we know the method is going to throw early — due to the title not being provided — we don’t have to spy on any external method because it will never be called in this case.

After we call the function with the faked arguments, we expect it to throw an error. Here, we should be specific. We shouldn’t expect any kind of error to be thrown, because it may as well be a typo in our code that would throw the error. It is better to expect that the error is, for example, an instance of a specific class. In our case, that would be a BadRequestException.

After calling Jest’s .expect(value) method, an object containing Jest’s matches is returned. Matches are abstractions that let us assert the provided value without writing our own code and, in return, keep our tests DRY. Using the matchers significantly shortens the test code and improves readability.

An example of a matcher would be the .toEqual() method that checks whether two objects are the same. It does so by comparing their keys and values recurrently, whereas Jest’s .toBe() method would simply check the strict equality using the === operator.

Check the documentation on Jest’s built-in matchers for more.

The first scenario is done. Let’s move on to the so-called happy path in which everything goes as planned.

describe('creating a playlist', () => {
  it('throws an error when no title is provided', async () => {
    // ...
  });

  it('calls the repository with correct paramaters', async () => {
    const title = faker.lorem.sentence();

    const createPlaylistData: CreatePlaylistData = {
      title,
    };

    const createdPlaylistEntity = Playlist.of(createPlaylistData);

    const savedPlaylist = Playlist.of({
      id: faker.random.uuid(),
      createdAt: new Date(),
      updatedAt: new Date(),
      title,
    });

    const playlistRepositorySaveSpy = jest
      .spyOn(playlistRepository, 'save')
      .mockResolvedValue(savedPlaylist);

    const playlistRepositoryCreateSpy = jest
      .spyOn(playlistRepository, 'create')
      .mockReturnValue(createdPlaylistEntity);

    const result = await playlistService.createOne(createPlaylistData);

    expect(playlistRepositoryCreateSpy).toBeCalledWith(createPlaylistData);
    expect(playlistRepositorySaveSpy).toBeCalledWith(createdPlaylistEntity);
    expect(result).toEqual(savedPlaylist);
  });
});

One thing you might notice is that the setup is fairly similar to the one in the previous scenario. The setup will always look somewhat the same because of the AAA (arrange, act, assert) pattern. The pattern is rather self-explanatory, but it suggests doing things in the specified order.

First, we set up all the fake data needed for the test. Then, we call the appropriate piece of code (in our case, the method) and we use Jest’s .expect() function to assert that the results are those we expected. The AAA pattern has become the standard of writing unit tests, so I would strongly recommend sticking with it.

Now, before we call the method with the fake arguments, we create the spies. In this case, we need spies for two external methods: playlistRepository.create() and playlistRepository.save(), both of which we mock (to track the calls) and stub (to override the returned value).

After we call the method, we expect that the result is the playlist that was “saved” in the database.

In order to check that the mocks have been called, we can use the expect(mock).toHaveBeenCalledWith() method and provide the arguments with which we expected the mocks to have been called.

After writing these two test scenarios, the one with the error and the happy path, we can now be sure that, assuming the dependencies work exactly like their contracts (the provided mocks and stubs), the .createOne() method works correctly.

Testing the other three methods

Given that each test follows the AAA pattern, there’s no point in repeating the explanation for each and every one of them.

Tests for the three remaining methods — .updateOne() , .findOne(), and .removeOne() — look more or less the same.

describe('PlaylistService', () => {
  let playlistService: PlaylistService;
  let playlistRepository: Repository<Playlist>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PlaylistService,
        {
          provide: getRepositoryToken(Playlist),
          useClass: PlaylistRepositoryFake,
        },
      ],
    }).compile();

    playlistService = module.get(PlaylistService);
    playlistRepository = module.get(getRepositoryToken(Playlist));
  });

  describe('updating a playlist', () => {
    it('calls the repository with correct paramaters', async () => {
      const playlistId = faker.random.uuid();
      const title = faker.lorem.sentence();

      const updatePlaylistData: UpdatePlaylistData = {
        id: playlistId,
        title,
      };

      const existingPlaylist = Playlist.of({
        id: playlistId,
        createdAt: new Date(),
        updatedAt: new Date(),
        title: faker.lorem.word(),
      });

      const newPlaylistData = Playlist.of({
        ...existingPlaylist,
        title,
      });

      const savedPlaylist = Playlist.of({
        ...newPlaylistData,
      });

      const playlistServiceFindOneByIdOrThrowSpy = jest
        .spyOn(playlistService, 'findOneByIdOrThrow')
        .mockResolvedValue(existingPlaylist);

      const playlistRepositoryCreateSpy = jest
        .spyOn(playlistRepository, 'create')
        .mockReturnValue(newPlaylistData);

      const playlistRepositorySaveSpy = jest
        .spyOn(playlistRepository, 'save')
        .mockResolvedValue(savedPlaylist);

      const result = await playlistService.updateOne(updatePlaylistData);

      expect(playlistServiceFindOneByIdOrThrowSpy).toHaveBeenCalledWith(
        updatePlaylistData.id,
      );

      expect(playlistRepositoryCreateSpy).toHaveBeenCalledWith({
        ...existingPlaylist,
        title,
      });

      expect(playlistRepositorySaveSpy).toHaveBeenCalledWith(newPlaylistData);
      expect(result).toEqual(savedPlaylist);
    });
  });

  describe('removing a playlist', () => {
    it('calls the repository with correct paramaters', async () => {
      const playlistId = faker.random.uuid();

      const removePlaylistData: RemovePlaylistData = {
        id: playlistId,
      };

      const existingPlaylist = Playlist.of({
        id: playlistId,
        createdAt: new Date(),
        updatedAt: new Date(),
        title: faker.lorem.sentence(),
      });

      const playlistServiceFindOneByIdOrThrowSpy = jest
        .spyOn(playlistService, 'findOneByIdOrThrow')
        .mockResolvedValue(existingPlaylist);

      const playlistRepositoryRemoveSpy = jest
        .spyOn(playlistRepository, 'remove')
        .mockResolvedValue(null);

      const result = await playlistService.removeOne(removePlaylistData);

      expect(playlistServiceFindOneByIdOrThrowSpy).toHaveBeenCalledWith(
        removePlaylistData.id,
      );

      expect(playlistRepositoryRemoveSpy).toHaveBeenCalledWith([
        existingPlaylist,
      ]);

      expect(result).toBe(null);
    });
  });

  describe('creating a playlist', () => {
    it('throws an error when no title is provided', async () => {
      const title = '';

      expect.assertions(2);

      try {
        await playlistService.createOne({ title });
      } catch (e) {
        expect(e).toBeInstanceOf(BadRequestException);
        expect(e.message).toBe('Title is required.');
      }
    });

    it('calls the repository with correct paramaters', async () => {
      const title = faker.lorem.sentence();

      const createPlaylistData: CreatePlaylistData = {
        title,
      };

      const createdPlaylistEntity = Playlist.of(createPlaylistData);

      const savedPlaylist = Playlist.of({
        id: faker.random.uuid(),
        createdAt: new Date(),
        updatedAt: new Date(),
        title,
      });

      const playlistRepositorySaveSpy = jest
        .spyOn(playlistRepository, 'save')
        .mockResolvedValue(savedPlaylist);

      const playlistRepositoryCreateSpy = jest
        .spyOn(playlistRepository, 'create')
        .mockReturnValue(createdPlaylistEntity);

      const result = await playlistService.createOne(createPlaylistData);

      expect(playlistRepositoryCreateSpy).toBeCalledWith(createPlaylistData);
      expect(playlistRepositorySaveSpy).toBeCalledWith(createdPlaylistEntity);
      expect(result).toEqual(savedPlaylist);
    });
  });

  describe('finding a playlist', () => {
    it('throws an error when a playlist doesnt exist', async () => {
      const playlistId = faker.random.uuid();

      const playlistRepositoryFindOneSpy = jest
        .spyOn(playlistRepository, 'findOne')
        .mockResolvedValue(null);

      expect.assertions(3);

      try {
        await playlistService.findOneByIdOrThrow(playlistId);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
        expect(e.message).toBe('No playlist found.');
      }

      expect(playlistRepositoryFindOneSpy).toHaveBeenCalledWith({
        id: playlistId,
      });
    });

    it('returns the found playlist', async () => {
      const playlistId = faker.random.uuid();

      const existingPlaylist = Playlist.of({
        id: playlistId,
        createdAt: new Date(),
        updatedAt: new Date(),
        title: faker.lorem.sentence(),
      });

      const playlistRepositoryFindOneSpy = jest
        .spyOn(playlistRepository, 'findOne')
        .mockResolvedValue(existingPlaylist);

      const result = await playlistService.findOneByIdOrThrow(playlistId);

      expect(result).toBe(existingPlaylist);
      expect(playlistRepositoryFindOneSpy).toHaveBeenCalledWith({
        id: playlistId,
      });
    });
  });
});

Each test first generates the fake data, then sets up spies, executes the method, and expects the result and the mocks to be a certain way.

Running the tests

Nest’s CLI-generated projects are automatically configured to run tests by simply typing npm run test in the terminal. All in all, with Jest, running the test usually comes down to executing the Jest command.

During development, it is really helpful to have the tests run after each change to the code, so Jest also provides us with the --watch option to do just that. More on setting up Jest with a more case-oriented config can be found in the docs.

Conclusion

Unit testing our application is the first step to creating more resilient and robust software. Running said tests on each commit (locally) and on a CI service can greatly improve our confidence.

Thanks to Nest — or, generally speaking, any kind of framework that makes use of dependency injection — code is easily testable thanks to all of its dependencies being managed outside the implementation, which, in turn, makes them easily replaceable.

Having unit tests in place also helps during refactoring because we are sure we didn’t break anything while trying to improve our codebase.

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Maciej Cieślar A JavaScript developer and a blogger @ https://www.mcieslar.com/

Leave a Reply