7

API with NestJS #87. Writing unit tests in a project with raw SQL

 1 year ago
source link: https://wanago.io/2022/12/19/api-nestjs-unit-tests-raw-sql/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

December 19, 2022

This entry is part 87 of 87 in the API with NestJS

Writing tests is crucial when aiming to develop a solid and reliable application. In this article, we explain the idea behind unit tests and write them for our application that works with raw SQL queries.

The idea behind unit tests

The job of a unit test is to make sure that an individual part of our application works as expected. Every test should be isolated and independent.

authentication.service.test.ts
import { AuthenticationService } from './authentication.service';
import UsersService from '../users/users.service';
import UsersRepository from '../users/users.repository';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import DatabaseService from '../database/database.service';
import { Pool } from 'pg';
describe('The AuthenticationService', () => {
  let authenticationService: AuthenticationService;
  beforeEach(() => {
    authenticationService = new AuthenticationService(
      new UsersService(new UsersRepository(new DatabaseService(new Pool()))),
      new JwtService({
        secretOrPrivateKey: 'Secret key',
      new ConfigService(),
  describe('when calling the getCookieForLogOut method', () => {
    it('should return a correct string', () => {
      const result = authenticationService.getCookieForLogOut();
      expect(result).toBe('Authentication=; HttpOnly; Path=/; Max-Age=0');

PASS src/authentication/authentication.service.test.ts
The AuthenticationService
when calling the getCookieForLogOut method
✓ should return a correct string

Above, you can see that we use the constructor of the AuthenticationService class. While we can provide all necessary dependencies manually, as in the example above, NestJS provides some utilities to help us.

By using the Test.createTestingModule method, we create a testing module. By doing that, we mock the entire NestJS runtime. Then, when we run its compile() method, we bootstrap the module with its dependencies in a similar way that our main.ts file works.

authentication.service.test.ts
import { AuthenticationService } from './authentication.service';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Test } from '@nestjs/testing';
import { UsersModule } from '../users/users.module';
import DatabaseModule from '../database/database.module';
describe('The AuthenticationService', () => {
  let authenticationService: AuthenticationService;
  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [AuthenticationService],
      imports: [
        UsersModule,
        ConfigModule.forRoot(),
        JwtModule.register({
          secretOrPrivateKey: 'Secret key',
        DatabaseModule.forRootAsync({
          imports: [ConfigModule],
          inject: [ConfigService],
          useFactory: (configService: ConfigService) => ({
            host: configService.get('POSTGRES_HOST'),
            port: configService.get('POSTGRES_PORT'),
            user: configService.get('POSTGRES_USER'),
            password: configService.get('POSTGRES_PASSWORD'),
            database: configService.get('POSTGRES_DB'),
    }).compile();
    authenticationService = await module.get(
      AuthenticationService,
  describe('when calling the getCookieForLogOut method', () => {
    it('should return a correct string', () => {
      const result = authenticationService.getCookieForLogOut();
      expect(result).toBe('Authentication=; HttpOnly; Path=/; Max-Age=0');

Mocking the database connection

There is a very significant problem with the above test suite. Importing our DatabaseModule class causes our application to try to connect to an actual database. This is something we definitely want to avoid when writing unit tests.

Simply removing the DatabaseModule from the imports array causes the following error:

Error: Nest can’t resolve dependencies of the UsersRepository (?). Please make sure that the argument DatabaseService at index [0] is available in the UsersModule context.

We need to acknowledge that the UsersService uses the database under the hood. To solve this problem, we can provide a mocked version of the UsersService class that does not use a real database.

It might be a good idea to avoid mocking whole modules when writing unit tests. This is because e don’t want to test how modules and classes interact with each other just yet.

authentication.service.test.ts
import { AuthenticationService } from './authentication.service';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '@nestjs/config';
import { Test } from '@nestjs/testing';
import UsersService from '../users/users.service';
import RegisterDto from './dto/register.dto';
describe('The AuthenticationService', () => {
  let registrationData: RegisterDto;
  let authenticationService: AuthenticationService;
  beforeEach(async () => {
    registrationData = {
      email: '[email protected]',
      name: 'John',
      password: 'strongPassword123',
    const module = await Test.createTestingModule({
      providers: [
        AuthenticationService,
          provide: UsersService,
          useValue: {
            create: jest.fn().mockReturnValue(registrationData),
      imports: [
        ConfigModule.forRoot(),
        JwtModule.register({
          secretOrPrivateKey: 'Secret key',
    }).compile();
    authenticationService = await module.get(
      AuthenticationService,
  describe('when calling the getCookieForLogOut method', () => {
    it('should return a correct string', () => {
      const result = authenticationService.getCookieForLogOut();
      expect(result).toBe('Authentication=; HttpOnly; Path=/; Max-Age=0');
  describe('when registering a new user', () => {
    describe('and when the usersService returns the new user', () => {
      it('should return the new user', async () => {
        const result = await authenticationService.register(registrationData);
        expect(result).toBe(registrationData);

PASS src/authentication/authentication.service.test.ts
The AuthenticationService
when calling the getCookieForLogOut method
✓ should return a correct string
when registering a new user
and when the usersService returns the new user
✓ should return the new user

Thanks to mocking the UsersService, we are confident our tests won’t need the real database.

Changing the mock per test

In the above test, we always assume that the create method of the AuthenticationService returns a valid user. However, this is not always the case.

There are two major cases for the AuthenticationService.register methods:

  • it returns the created user if there weren’t any problems with the data,
  • it throws an error if the user with a given email address already exists.

Fortunately, we can change our mock per test. However, to do that, we need to ensure that the mock is accessible through every test. We can achieve that by creating a variable that we modify through the beforeEach hook.

Thanks to using beforeEach we ensure that each test is independent and does not affect the other tests.

authentication.service.test.ts
import { AuthenticationService } from './authentication.service';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '@nestjs/config';
import { Test } from '@nestjs/testing';
import UsersService from '../users/users.service';
import RegisterDto from './dto/register.dto';
describe('The AuthenticationService', () => {
  let registrationData: RegisterDto;
  let authenticationService: AuthenticationService;
  let createUserMock: jest.Mock;
  beforeEach(async () => {
    registrationData = {
      email: '[email protected]',
      name: 'John',
      password: 'strongPassword123',
    createUserMock = jest.fn();
    const module = await Test.createTestingModule({
      providers: [
        AuthenticationService,
          provide: UsersService,
          useValue: {
            create: createUserMock,
      imports: [
        ConfigModule.forRoot(),
        JwtModule.register({
          secretOrPrivateKey: 'Secret key',
    }).compile();
    authenticationService = await module.get(
      AuthenticationService,
  // ...

Thanks to creating the createUserMock variable accessible in the whole test suite, we now have full control over it. We can now manipulate it to cover both of the above cases.

authentication.service.test.ts
import { AuthenticationService } from './authentication.service';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '@nestjs/config';
import { Test } from '@nestjs/testing';
import UsersService from '../users/users.service';
import UserAlreadyExistsException from '../users/exceptions/userAlreadyExists.exception';
import { BadRequestException } from '@nestjs/common';
import RegisterDto from './dto/register.dto';
describe('The AuthenticationService', () => {
  let registrationData: RegisterDto;
  let authenticationService: AuthenticationService;
  let createUserMock: jest.Mock;
  beforeEach(async () => {
    registrationData = {
      email: '[email protected]',
      name: 'John',
      password: 'strongPassword123',
    createUserMock = jest.fn();
    const module = await Test.createTestingModule({
      providers: [
        AuthenticationService,
          provide: UsersService,
          useValue: {
            create: createUserMock,
      imports: [
        ConfigModule.forRoot(),
        JwtModule.register({
          secretOrPrivateKey: 'Secret key',
    }).compile();
    authenticationService = await module.get(
      AuthenticationService,
  describe('when calling the getCookieForLogOut method', () => {
    it('should return a correct string', () => {
      const result = authenticationService.getCookieForLogOut();
      expect(result).toBe('Authentication=; HttpOnly; Path=/; Max-Age=0');
  describe('when registering a new user', () => {
    describe('and when the usersService returns the new user', () => {
      beforeEach(() => {
        createUserMock.mockReturnValue(registrationData);
      it('should return the new user', async () => {
        const result = await authenticationService.register(registrationData);
        expect(result).toBe(registrationData);
    describe('and when the usersService throws the UserAlreadyExistsException', () => {
      beforeEach(() => {
        createUserMock.mockImplementation(() => {
          throw new UserAlreadyExistsException(registrationData.email);
      it('should throw the BadRequestException', () => {
        return expect(() =>
          authenticationService.register(registrationData),
        ).rejects.toThrow(BadRequestException);

PASS src/authentication/authentication.service.test.ts
The AuthenticationService
when calling the getCookieForLogOut method
✓ should return a correct string
when registering a new user
and when the usersService returns the new user
✓ should return the new user
and when the usersService throws the UserAlreadyExistsException
✓ should throw the BadRequestException

Above, we are covering two separate cases:

  • when the usersService returns the new user,
  • when the usersService throws an error.

The crucial thing to notice is that we are not testing the UsersService. Our tests focus solely on the methods of the AuthenticationService class, which is the essence of the unit tests. When writing unit tests, we don’t verify how classes work together but rather ensure they perform in isolation.

Testing the data repository

So far, in this article, we didn’t test a class that directly uses our DatabaseService. Let’s test the create method of our UsersRepository.

users.repository.test.ts
import { CreateUserDto } from './dto/createUser.dto';
import { Test } from '@nestjs/testing';
import DatabaseService from '../database/database.service';
import UsersRepository from './users.repository';
import UserModel, { UserModelData } from './user.model';
import DatabaseError from '../types/databaseError';
import PostgresErrorCode from '../database/postgresErrorCode.enum';
import UserAlreadyExistsException from './exceptions/userAlreadyExists.exception';
describe('The UsersRepository class', () => {
  let runQueryMock: jest.Mock;
  let createUserData: CreateUserDto;
  let usersRepository: UsersRepository;
  beforeEach(async () => {
    createUserData = {
      name: 'John',
      email: '[email protected]',
      password: 'strongPassword123',
    runQueryMock = jest.fn();
    const module = await Test.createTestingModule({
      providers: [
        UsersRepository,
          provide: DatabaseService,
          useValue: {
            runQuery: runQueryMock,
    }).compile();
    usersRepository = await module.get(UsersRepository);
  describe('when the create method is called', () => {
    describe('and the database returns valid data', () => {
      let userModelData: UserModelData;
      beforeEach(() => {
        userModelData = {
          id: 1,
          name: 'John',
          email: '[email protected]',
          password: 'strongPassword123',
          address_id: null,
          address_street: null,
          address_city: null,
          address_country: null,
        runQueryMock.mockResolvedValue({
          rows: [userModelData],
      it('should return an instance of the UserModel', async () => {
        const result = await usersRepository.create(createUserData);
        expect(result instanceof UserModel).toBe(true);
      it('should return the UserModel with correct properties', async () => {
        const result = await usersRepository.create(createUserData);
        expect(result.id).toBe(userModelData.id);
        expect(result.email).toBe(userModelData.email);
        expect(result.name).toBe(userModelData.name);
        expect(result.password).toBe(userModelData.password);
        expect(result.address).not.toBeDefined();
    describe('and the database throws the UniqueViolation', () => {
      beforeEach(() => {
        const databaseError: DatabaseError = {
          code: PostgresErrorCode.UniqueViolation,
          table: 'users',
          detail: 'Key (email)=([email protected]) already exists.',
        runQueryMock.mockImplementation(() => {
          throw databaseError;
      it('should throw the UserAlreadyExistsException exception', () => {
        return expect(() =>
          usersRepository.create(createUserData),
        ).rejects.toThrow(UserAlreadyExistsException);

PASS src/users/users.repository.test.ts
The UsersRepository class
when the create method is called
and the database returns valid data
✓ should return an instance of the UserModel
✓ should return the UserModel with correct properties
and the database throws the UniqueViolation
✓ should throw the UserAlreadyExistsException exception

Above, we are testing two crucial cases of the create method:

  • the user is successfully added to the database,
  • the database fails to add the user due to the unique violation error.

Feel free to take it a step further and test if the UsersRepository makes the right SQL query.

Summary

In this article, we’ve learned what unit tests are and how to implement them with NestJS. We’ve focused on testing the parts of our application that communicate with a database. Since unit tests shouldn’t use a real database connection, we’ve learned how to mock our services and repositories. There is still more to learn when it comes to testing NestJS when using SQL, so stay tuned!

Series Navigation<< API with NestJS #86. Logging with the built-in logger when using raw SQL


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK