Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Unit tests

A unit test checks one class apart from the rest of the application. The posts service needs a repository to work, but a test does not need a real database: the repository is replaced with a mock — an object with the same methods that return predefined data. A NestJS project is already set up with Jest, a library for running tests.

Where tests live

A test sits next to its class and has the same name ending in .spec.ts: for src/posts/posts.service.ts it is src/posts/posts.service.spec.ts. nest g service creates this file for you.

npm run test

The command finds every *.spec.ts file in the src folder and runs it.

The testing module

Test.createTestingModule() builds a small module from just the providers the test needs:

// src/posts/posts.service.spec.ts
import { NotFoundException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Post } from './entities/post.entity';
import { PostsService } from './posts.service';
 
describe('PostsService', () => {
  let service: PostsService;
  const repository = {
    findOne: jest.fn(),
    find: jest.fn(),
  };
 
  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      providers: [
        PostsService,
        { provide: getRepositoryToken(Post), useValue: repository },
      ],
    }).compile();
 
    service = moduleRef.get(PostsService);
    jest.clearAllMocks();
  });
 
  it('returns a post by id', async () => {
    repository.findOne.mockResolvedValue({ id: 1, title: 'First post' });
 
    const post = await service.findOne(1);
 
    expect(post.title).toBe('First post');
  });
 
  it('throws 404 when the post does not exist', async () => {
    repository.findOne.mockResolvedValue(null);
 
    await expect(service.findOne(999)).rejects.toThrow(NotFoundException);
  });
});

getRepositoryToken(Post) is the key under which NestJS keeps the posts repository. The { provide, useValue } entry says: "instead of the real repository, hand out this object". jest.fn() creates a mock function, and mockResolvedValue() sets the promise it returns.

The result

 PASS  src/posts/posts.service.spec.ts
  PostsService
    √ returns a post by id (5 ms)
    √ throws 404 when the post does not exist (3 ms)
 
Tests:       2 passed, 2 total

If you break findOne() — for example, remove the null check — the second test shows FAIL and explains that an exception was expected.

Checking calls

A mock remembers how it was called:

// src/posts/posts.service.spec.ts
it('loads the post together with comments', async () => {
  repository.findOne.mockResolvedValue({ id: 1 });
 
  await service.findOne(1);
 
  expect(repository.findOne).toHaveBeenCalledWith({
    where: { id: 1 },
    relations: { comments: true },
  });
});

Useful commands

CommandWhat it does
npm run testRuns all unit tests once
npm run test:watchReruns tests when files change
npm run test:covShows how much of the code is covered by tests

Common mistake

A test fails with Nest can't resolve dependencies of the PostsService (?). The testing module knows nothing about TypeOrmModule — every dependency of the service must be passed to providers yourself, real or mocked. The test generated by the CLI contains only the service, so after adding a repository you must extend it.

Thanks for reading!

Donate to the site — we work hard for you.