Writing unit tests in a project with raw SQL
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…
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: 'john@smith.com', 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: 'john@smith.com', 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: 'john@smith.com', 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(