6 min read
Original source

Unit tests with the Drizzle ORM

Unit tests play a significant role in ensuring the reliability of our NestJS application. In this article, we’ll explain the concept behind unit testing and…

Unit tests play a significant role in ensuring the reliability of our NestJS application. In this article, we’ll explain the concept behind unit testing and learn how to apply it to a NestJS application with the Drizzle ORM. Introduction to unit tests Unit tests allow us to verify that individual parts of our codebase function as expected on their own. NestJS is configured to handle tests using the Jest framework out of the box. When we run the npm run test command, Jest finds files that follow a specific naming pattern. By default, NestJS is set up to look for files that end with .spec.ts. Alternatively, we can configure it to handle files that end with .test.ts. To do that, we need to adjust the package.json file.{ // ... "jest": { "testRegex": ".*\\.(spec|test)\\.ts$", // ... } } Thanks to the above regular expression, Jest will pick up files that end with eiter .spec.ts or .test.ts. Writing our first unit test Let’s take a look at the getCookieForLogOut function in our AuthenticationService. authentication.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class AuthenticationService { getCookieForLogOut() { return `Authentication=; HttpOnly; Path=/; Max-Age=0`; } // ... }Let’s write a test ensuring our method returns a valid string. authentication.service.test.ts import { AuthenticationService } from './authentication.service'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { UsersService } from '../users/users.service'; import { DrizzleService } from '../database/drizzle.service'; import { Pool } from 'pg'; describe('The AuthenticationService', () => { let authenticationService: AuthenticationService; beforeEach(() => { const configService = new ConfigService(); authenticationService = new AuthenticationService( new UsersService( new DrizzleService( new Pool({ 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'), }), ), ), 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 In our approach above, we call the AuthenticationService constructor manually. Alternatively, we can use the NestJS test utilities to handle that instead. To achieve that, we need the Test.createTestingModule method from the @nestjs/testing library. 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'); }); }); });With this alternative solution, we create a mock of the NestJS runtime. By calling the compile() method, we set up a module with its dependencies similar to how the bootstrap function in our main.ts file works. Avoiding a real database It’s crucial to notice that our DatabaseModule connects to a real PostgreSQL database. If our database has any issues, our tests could fail. We don’t want that because unit tests should be independent and reliable. One way of solving this problem is to recognize that it’s the UsersService class that uses the database under the hood. If we use a mocked version of the UsersService, we no longer need to connect to the database. 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 { SignUpDto } from './dto/sign-up.dto'; describe('The AuthenticationService', () => { let signUpData: SignUpDto; let authenticationService: AuthenticationService; beforeEach(async () => { signUpData = { email: 'john@smith.com', name: 'John', password: 'strongPassword123', }; const module = await Test.createTestingModule({ providers: [ AuthenticationService, { provide: UsersService, useValue: { create: jest.fn().mockReturnValue(signUpData), }, }, ], imports: [ ConfigModule.forRoot(), JwtModule.register({ secretOrPrivateKey: 'Secret key', }), ], }).compile(); authenticationService = await module.get(AuthenticationService); }); // ... 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.signUp(signUpData); expect(result).toBe(signUpData); }); }); }); }); Adjusting the mock for each test In our test above, we mock the create method in the UsersService to return a valid user each time. However, a particular method can yield various results in different situations. For example, the getByEmail method: if we provide a valid email of a user who’s signed up, it returns the user, when a user with the provided email does not exist, it throws the NotFoundException. Let’s create a mock that can cover both cases. To do that, let’s create the getByEmailMock variable and use it in the mocked UsersService. Then, let’s use getByEmailMock.mockResolvedValue function when we want the getByEmail method to return a value successfully. When we want it to fail, we have to use getByEmailMock.mockRejectedValue. 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 { SignUpDto } from './dto/sign-up.dto'; import * as bcrypt from 'bcrypt'; import { NotFoundException } from '@nestjs/common'; import { InferSelectModel } from 'drizzle-orm'; import { databaseSchema } from '../database/database-schema'; import { WrongCredentialsException } from './wrong-credentials-exception'; describe('The AuthenticationService', () => { let signUpData: SignUpDto; let authenticationService: AuthenticationService; let getByEmailMock: jest.Mock; let password: string; beforeEach(async () => { getByEmailMock = jest.fn(); password = 'strongPassword123'; signUpData = { email: 'john@smith.com', name: 'John', password: 'strongPassword123', }; const module = await Test.createTestingModule({ providers: [ AuthenticationService, { provide: UsersService, useValue: { create: jest.fn().mockReturnValue(signUpData), getByEmail: getByEmailMock, }, }, ], imports: [ ConfigModule.forRoot(), JwtModule.register({ secretOrPrivateKey: 'Secret key', }), ], }).compile(); authenticationService = await module.get(AuthenticationService); }); // ... describe('when the getAuthenticatedUser method is called', () => { describe('and a valid email and password are provided', () => { let userData: InferSelectModel; beforeEach(async () => { const hashedPassword = await bcrypt.hash(password, 10); userData = { id: 1, email: 'john@smith.com', name: 'John', password: hashedPassword, addressId: null, }; getByEmailMock.mockResolvedValue(userData); }); it('should return the new user', async () => { const result = await authenticationService.getAuthenticatedUser({ email: userData.email, password, }); expect(result).toBe(userData); }); }

Unit tests with the Drizzle ORM | NestJS.io