DEV Community

J.D Nicholls
J.D Nicholls

Posted on

Testing with NestJS like a Pro

Hello folks,

I've been using NestJS for the last few years and it's an amazing framework for NodeJS and ExpressJS. I personally love using TypeScript on the Backend and having a good Separation of concerns (SoC) while I'm creating my services, following a technology-agnostic approach for the win!

But sometimes it's difficult to do certain things, especially the tests for our code, which is something that's hardly mentioned (even in documentation). This is why when you want to use TypeORM to perform more advanced operations (revert SQL transactions if something fails, etc) it can become difficult to test our code.

Likewise, sometimes it's difficult to mock all these external dependencies of our services and it becomes somewhat tedious, so we stop enjoying working with these tools that should make our lives easier instead of wanting to change our entire development.

That is why in this post I want to show you an amazing library to create mocks of our dependencies using Jest and how to use a design pattern such as Singleton, which has helped me a lot when testing my code.

Imagine we have this strange microservice:

import { WRITE_CONNECTION } from '@my-api/common';
import { Injectable, Logger } from '@nestjs/common';
import { RpcException } from '@nestjs/microservices';
import { InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager } from 'typeorm';

@Injectable()
export class MyService {
  private logger = new Logger(MyService.name);
  constructor(
    @InjectEntityManager(WRITE_CONNECTION) private entityManager: EntityManager,
  ) {}

  async saveSomething(data: string): Promise<void> {
    try {
      return await this.entityManager.transaction(async (entityManager) => {
        const firstRepository = entityManager.getCustomRepository(FirstRepository);
        const secondRepository = entityManager.getCustomRepository(SecondRepository);

        const firstRecord = firstRepository.create({ data });
        await firstRepository.save(firstRecord);

        const secondRecord = secondRepository.create({ data });
        await secondRepository.save(secondRecord);

        // Save entities directly
        await entityManager.save([...]);
      });
    } catch (error) {
      this.logger.error(`Failed saving something, error ${error.message}`, error.stack);
      throw new RpcException(error.message);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now let's see what it would be like to test our code:

import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { EntityManager } from 'typeorm';

describe('MyService', () => {
  let service: MyService;
  let entityManager: EntityManager;
  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        MyService,
        { provide: EntityManager, useValue: createMock<EntityManager>() },
      ],
    }).compile();

    service = module.get(CatalogsService);
    entityManager = module.get(EntityManager);
  });
});
Enter fullscreen mode Exit fullscreen mode

As we can see, it's very easy to mock these external services such as the TypeORM EntityManager, etc. using the createMock function provided by Go Level Up, which automatically injects Jest mock functions to replace external implementations on which our code depends, which in a unit test should not matter.

import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { EntityManager } from 'typeorm';

describe('MyService', () => {
  ...

  it('should call a transaction correctly', async () => {
    const mockEntityManager = {
      save: jest.fn(),
      getCustomRepository: jest.fn((fn) => mockEntityManager[fn] || (mockEntityManager[fn] = createMock<typeof fn>())),
    };
    const spyTransaction = (entityManager.transaction as jest.Mock).mockImplementation((cb) => cb(mockEntityManager));
    const firstRepo: FirstRepository = mockEntityManager.getCustomRepository(SubCatalogRepository);
    const secondRepo: SecondRepository = mockEntityManager.getCustomRepository(SecondRepository);
    await service.saveSomething('MOCK DATA');

    expect(spyTransaction).toHaveBeenCalled();
    expect(firstRepo.save).toHaveBeenCalled();
    expect(secondRepo.save).toHaveBeenCalled();
    expect(mockEntityManager.save).toHaveBeenCalled();
  });
Enter fullscreen mode Exit fullscreen mode

Then we have the possibility of using the EntityManager to create transactions with several repositories that can execute a rollback automatically if any of these operations fail, and in the tests we use the Singleton pattern to define a mock of this entity that allows us to return the same instance of these repositories to test that all these read and write operations have been performed as expected.

Remember that in our tests it's also important to test not only the happy path, but all kinds of scenarios where our code can fail due to some invalid or not allowed operation. For this, with Jest we have utilities where we can easily test these asynchronous cases. e.g:

  • my-service.ts:
@Injectable()
export class MyService {
  private logger = new Logger(MyService.name);

  constructor(
    private myRepository: MyRepository,
  ) {}

  async donwloadReport(recordId: number): Promise<string> {
    try {
      const record = await this.myRepository.findOne(recordId);
      if (!record) {
        throw new Error('Record not found');
      }
      // TODO: generate CSV file or something like that
      return 'export';
    } catch (error) {
      this.logger.error(`Failed generating a report, error ${error.message}`, error.stack);
      throw new RpcException(error.message);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  • my-service.spec.ts:
describe('MyService', () => {
  let service: MyService;
  let repository: MyRepository;
  let logger: Logger;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        MyService,
        { provide: MyRepository, useValue: createMock<MyRepository>() },
      ],
    }).compile();

    service = module.get<ProductsService>(ProductsService);
    repository = module.get(BrandRepository);
    logger = service['logger'] = createMock<Logger>();
  });

  it('should throw an error when downloading a report of a record that does not exist', async () => {
    const errorMessage = 'Record not found';
    const spyFindOne = (repository.findOne as jest.Mock).mockImplementationOnce(() => Promise.resolve(null));
    const recordId = -1;
    await expect(service.downloadReport(recordId)).rejects.toThrow(new RpcException(errorMessage));
    expect(spyFindOne).toHaveBeenCalledWith(recordId);
    expect(logger.error).toHaveBeenCalled();
  });
});
Enter fullscreen mode Exit fullscreen mode

Using expect().rejects we can wait for our asynchronous code to fail and throw an exception handled by our code, thus avoiding undesirable scenarios where the client is responded with an Internal Server Error which was not anticipated by a noob developer.

Bonus:

If you want to learn more about design patterns, don't forget to take a look at Design Patterns for Humans, it's an incredible repository with many interesting examples that you can apply when you want to use a design pattern to solve a specific problem.

I hope you find this example useful for your projects, and let's continue improving our services with NestJS! 😊

Supporting 🍻

I believe in Unicorns 🦄 Support me, if you do too.

Made with ❤️

J.D. Nicholls

Top comments (1)

Collapse
 
jdnichollsc profile image
J.D Nicholls

This article explains how to Mock EntityManager for Unit testing, issue related: github.com/typeorm/typeorm/issues/...

Happy Coding! <3