Nest.js Tutorial

Unit tests with the Drizzle ORM

Marcin Wanago
NestJS

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.

1{
2  // ...
3  "jest": {
4    "testRegex": ".*\\.(spec|test)\\.ts$",
5    // ...
6  }
7}
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
1import { Injectable } from '@nestjs/common';
2 
3@Injectable()
4export class AuthenticationService {
5  getCookieForLogOut() {
6    return `Authentication=; HttpOnly; Path=/; Max-Age=0`;
7  }
8  
9  // ...
10}

Let’s write a test ensuring our method returns a valid string.

authentication.service.test.ts
1import { AuthenticationService } from './authentication.service';
2import { JwtService } from '@nestjs/jwt';
3import { ConfigService } from '@nestjs/config';
4import { UsersService } from '../users/users.service';
5import { DrizzleService } from '../database/drizzle.service';
6import { Pool } from 'pg';
7 
8describe('The AuthenticationService', () => {
9  let authenticationService: AuthenticationService;
10  beforeEach(() => {
11    const configService = new ConfigService();
12 
13    authenticationService = new AuthenticationService(
14      new UsersService(
15        new DrizzleService(
16          new Pool({
17            host: configService.get('POSTGRES_HOST'),
18            port: configService.get('POSTGRES_PORT'),
19            user: configService.get('POSTGRES_USER'),
20            password: configService.get('POSTGRES_PASSWORD'),
21            database: configService.get('POSTGRES_DB'),
22          }),
23        ),
24      ),
25      new JwtService({
26        secretOrPrivateKey: 'Secret key',
27      }),
28      new ConfigService(),
29    );
30  });
31  describe('when calling the getCookieForLogOut method', () => {
32    it('should return a correct string', () => {
33      const result = authenticationService.getCookieForLogOut();
34      expect(result).toBe('Authentication=; HttpOnly; Path=/; Max-Age=0');
35    });
36  });
37});
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
1import { AuthenticationService } from './authentication.service';
2import { JwtModule } from '@nestjs/jwt';
3import { ConfigModule, ConfigService } from '@nestjs/config';
4import { Test } from '@nestjs/testing';
5import { UsersModule } from '../users/users.module';
6import { DatabaseModule } from '../database/database.module';
7 
8describe('The AuthenticationService', () => {
9  let authenticationService: AuthenticationService;
10  beforeEach(async () => {
11    const module = await Test.createTestingModule({
12      providers: [AuthenticationService],
13      imports: [
14        UsersModule,
15        ConfigModule.forRoot(),
16        JwtModule.register({
17          secretOrPrivateKey: 'Secret key',
18        }),
19        DatabaseModule.forRootAsync({
20          imports: [ConfigModule],
21          inject: [ConfigService],
22          useFactory: (configService: ConfigService) => ({
23            host: configService.get('POSTGRES_HOST'),
24            port: configService.get('POSTGRES_PORT'),
25            user: configService.get('POSTGRES_USER'),
26            password: configService.get('POSTGRES_PASSWORD'),
27            database: configService.get('POSTGRES_DB'),
28          }),
29        }),
30      ],
31    }).compile();
32 
33    authenticationService = await module.get(AuthenticationService);
34  });
35  describe('when calling the getCookieForLogOut method', () => {
36    it('should return a correct string', () => {
37      const result = authenticationService.getCookieForLogOut();
38      expect(result).toBe('Authentication=; HttpOnly; Path=/; Max-Age=0');
39    });
40  });
41});

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
1import { AuthenticationService } from './authentication.service';
2import { JwtModule } from '@nestjs/jwt';
3import { ConfigModule } from '@nestjs/config';
4import { Test } from '@nestjs/testing';
5import { UsersService } from '../users/users.service';
6import { SignUpDto } from './dto/sign-up.dto';
7 
8describe('The AuthenticationService', () => {
9  let signUpData: SignUpDto;
10  let authenticationService: AuthenticationService;
11  beforeEach(async () => {
12    signUpData = {
13      email: 'john@smith.com',
14      name: 'John',
15      password: 'strongPassword123',
16    };
17 
18    const module = await Test.createTestingModule({
19      providers: [
20        AuthenticationService,
21        {
22          provide: UsersService,
23          useValue: {
24            create: jest.fn().mockReturnValue(signUpData),
25          },
26        },
27      ],
28      imports: [
29        ConfigModule.forRoot(),
30        JwtModule.register({
31          secretOrPrivateKey: 'Secret key',
32        }),
33      ],
34    }).compile();
35 
36    authenticationService = await module.get(AuthenticationService);
37  });
38  // ...
39  describe('when registering a new user', () => {
40    describe('and when the usersService returns the new user', () => {
41      it('should return the new user', async () => {
42        const result = await authenticationService.signUp(signUpData);
43        expect(result).toBe(signUpData);
44      });
45    });
46  });
47});

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
1import { AuthenticationService } from './authentication.service';
2import { JwtModule } from '@nestjs/jwt';
3import { ConfigModule } from '@nestjs/config';
4import { Test } from '@nestjs/testing';
5import { UsersService } from '../users/users.service';
6import { SignUpDto } from './dto/sign-up.dto';
7import * as bcrypt from 'bcrypt';
8import { NotFoundException } from '@nestjs/common';
9import { InferSelectModel } from 'drizzle-orm';
10import { databaseSchema } from '../database/database-schema';
11import { WrongCredentialsException } from './wrong-credentials-exception';
12 
13describe('The AuthenticationService', () => {
14  let signUpData: SignUpDto;
15  let authenticationService: AuthenticationService;
16  let getByEmailMock: jest.Mock;
17  let password: string;
18  beforeEach(async () => {
19    getByEmailMock = jest.fn();
20    password = 'strongPassword123';
21    signUpData = {
22      email: 'john@smith.com',
23      name: 'John',
24      password: 'strongPassword123',
25    };
26 
27    const module = await Test.createTestingModule({
28      providers: [
29        AuthenticationService,
30        {
31          provide: UsersService,
32          useValue: {
33            create: jest.fn().mockReturnValue(signUpData),
34            getByEmail: getByEmailMock,
35          },
36        },
37      ],
38      imports: [
39        ConfigModule.forRoot(),
40        JwtModule.register({
41          secretOrPrivateKey: 'Secret key',
42        }),
43      ],
44    }).compile();
45 
46    authenticationService = await module.get(AuthenticationService);
47  });
48  // ...
49  describe('when the getAuthenticatedUser method is called', () => {
50    describe('and a valid email and password are provided', () => {
51      let userData: InferSelectModel<typeof databaseSchema.users>;
52      beforeEach(async () => {
53        const hashedPassword = await bcrypt.hash(password, 10);
54        userData = {
55          id: 1,
56          email: 'john@smith.com',
57          name: 'John',
58          password: hashedPassword,
59          addressId: null,
60        };
61        getByEmailMock.mockResolvedValue(userData);
62      });
63      it('should return the new user', async () => {
64        const result = await authenticationService.getAuthenticatedUser({
65          email: userData.email,
66          password,
67        });
68        expect(result).toBe(userData);
69      });
70    });
71    describe('and an invalid email is provided', () => {
72      beforeEach(() => {
73        getByEmailMock.mockRejectedValue(new NotFoundException());
74      });
75      it('should throw the BadRequestException', () => {
76        return expect(async () => {
77          await authenticationService.getAuthenticatedUser({
78            email: 'john@smith.com',
79            password,
80          });
81        }).rejects.toThrow(WrongCredentialsException);
82      });
83    });
84  });
85});
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 when the getAuthenticatedUser method is called and a valid email and password are provided ✓ should return the new user and an invalid email is provided ✓ should throw the BadRequestException

Focusing on a single class or function is an essential aspect of unit testing. Therefore, we must focus on testing the methods of the AuthenticationService and ensure our tests don’t rely on the code of the UsersService. We must make sure the classes work as expected in isolation instead of checking how they work together.

Mocking the Drizzle ORM

We haven’t yet tested a class that uses the Drizzle ORM directly. Let’s take a look at the getById method in our UsersService.

users.service.ts
1import { Injectable, NotFoundException } from '@nestjs/common';
2import { DrizzleService } from '../database/drizzle.service';
3import { databaseSchema } from '../database/database-schema';
4import { eq } from 'drizzle-orm';
5 
6@Injectable()
7export class UsersService {
8  constructor(private readonly drizzleService: DrizzleService) {}
9 
10  async getById(id: number) {
11    const user = await this.drizzleService.db.query.users.findFirst({
12      with: {
13        address: true,
14      },
15      where: eq(databaseSchema.users.id, id),
16    });
17 
18    if (!user) {
19      throw new NotFoundException();
20    }
21 
22    return user;
23  }
24 
25  // ...
26}

There are two major cases in our getById method:

  • if the findFirst method returns the user, the getById should return it too,
  • if the findFirst method returns undefined, getById should throw the NotFoundException.

To test both situations, we need to mock the DrizzleService.

users.service.test.ts
1import { UsersService } from './users.service';
2import { Test } from '@nestjs/testing';
3import { NotFoundException } from '@nestjs/common';
4import { DrizzleService } from '../database/drizzle.service';
5import { InferSelectModel } from 'drizzle-orm';
6import { databaseSchema } from '../database/database-schema';
7 
8describe('The UsersService', () => {
9  let usersService: UsersService;
10  let findFirstMock: jest.Mock;
11  beforeEach(async () => {
12    findFirstMock = jest.fn();
13    const module = await Test.createTestingModule({
14      providers: [
15        UsersService,
16        {
17          provide: DrizzleService,
18          useValue: {
19            db: {
20              query: {
21                users: {
22                  findFirst: findFirstMock,
23                },
24              },
25            },
26          },
27        },
28      ],
29    }).compile();
30 
31    usersService = await module.get(UsersService);
32  });
33  describe('when the getById function is called', () => {
34    describe('and the findFirst method returns the user', () => {
35      let user: InferSelectModel<typeof databaseSchema.users>;
36      beforeEach(() => {
37        user = {
38          id: 1,
39          email: 'john@smith.com',
40          name: 'John',
41          password: 'strongPassword123',
42          addressId: null,
43        };
44        findFirstMock.mockResolvedValue(user);
45      });
46      it('should return the user', async () => {
47        const result = await usersService.getById(user.id);
48        expect(result).toBe(user);
49      });
50    });
51    describe('and the findFirst method does not return the user', () => {
52      beforeEach(() => {
53        findFirstMock.mockResolvedValue(undefined);
54      });
55      it('should throw the NotFoundException', async () => {
56        return expect(async () => {
57          await usersService.getById(1);
58        }).rejects.toThrow(NotFoundException);
59      });
60    });
61  });
62});

Thanks to adjusting the mock for each case using findFirstMock.mockResolvedValue, we can cover both cases.

PASS src/users/users.service.test.ts The UsersService when the getById function is called and the findFirst method returns the user ✓ should return the user and the findFirst method does not return the user ✓ should throw the NotFoundException

Summary

In this article, we’ve covered the basics of unit testing and how to apply it in a NestJS application. To practice, we’ve used services that use Drizzle ORM under the hood to connect to the database. Since unit tests should not rely on a real database connection, we learned how to mock services, including the Drizzle Service. While this provides a solid foundation for testing a NestJS application with Drizzle ORM, there is still more to learn. Stay tuned!