Applying integration test on NestJS with Jest and GitHub Actions
šŸ”

Applying integration test on NestJS with Jest and GitHub Actions

Tags
NestJS
Node.js
Typescript
auth
SWC
Passion Economy
Jest
Tests
Published
October 16, 2023
Author
henrique weiand
Ā 
Hello fellow coders! Today we are going to talk a bit about tests inside NestJS, letā€™s start looking at and applying one type of test and later we are going to see another, so the first one will be e2e.

Definition

ā€œEnd-to-end testing is a software testing technique that verifies the functionality and performance of an entire software application from start to finish by simulating real-world user scenarios and replicating live data. Its objective is to identify bugs that arise when all components are integrated, ensuring that the application delivers the expected output as a unified entity.ā€ - https://katalon.com/resources-center/blog/end-to-end-e2e-testing
Ā 
In this example, we are going to use the auth project (one that we did previously from another post) and focus on applying the e2e test inside the unique controller that this application has. Our focus will be to test the possible responses that we can have when a user interacts with the endpoints from the auth controller.
Ā 

Setting the project up

Even though the project is previously finished, we need to change one thing inside the setup to run our tests with the SWC. Letā€™s start with the dependencies
npm --save-dev @swc/jest
Ā 
Create inside the root directory a file called .swcrc
{ "$schema": "https://json.schemastore.org/swcrc", "sourceMaps": true, "jsc": { "parser": { "syntax": "typescript", "decorators": true, "dynamicImport": true }, "transform": { "legacyDecorator": true, "decoratorMetadata": true }, "baseUrl": "./" }, "minify": false }
Ā 
Let`s change the jest-e2e.json
notion image
Ā 
Inside the package.json, we also need to make some changes
notion image
Ā 
All these changes above are important to make the jest run integrated with SWC, but also to guarantee that the files are getting the right configuration.
Ā 

Creating the tests

Before we start, make sure you are running Postgres, in my case, inside the project folder I am going to run the command.
docker-compose up -d
Ā 
Letā€™s create the file auth.controller.e2e-spec.ts inside the auth folder, and our first test will be to test if we are able to create a new user.
Ā 
import { Test } from '@nestjs/testing'; import request from 'supertest'; import { Repository } from 'typeorm'; import { Users } from '../users/users.entity'; import { getRepositoryToken } from '@nestjs/typeorm'; import { HttpStatus } from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthModule } from './auth.module'; import { DatabaseModule } from '../database/database.module'; const testUsername = 'authControllerE2ETests'; describe('AuthController', () => { let app: any; let httpServer: any; let repository: Repository<Users>; let authService: AuthService; beforeAll(async () => { const module = await Test.createTestingModule({ imports: [DatabaseModule, AuthModule], }).compile(); app = module.createNestApplication(); authService = module.get<AuthService>(AuthService); repository = module.get<Repository<Users>>(getRepositoryToken(Users)); await app.init(); httpServer = app.getHttpServer(); }); afterAll(async () => { await app.close(); }); afterEach(async () => { await repository.delete({ username: testUsername, }); }); describe('SingUp', () => { it('should create a user', async () => { const createUserRequest: { username: string; password: string } = { username: testUsername, password: 'password', }; const response = await request(httpServer) .post('/auth/signUp') .send(createUserRequest); expect(response.status).toBe(HttpStatus.OK); expect(response.body).toHaveProperty( 'username', createUserRequest.username, ); expect(response.body).toHaveProperty('id'); const user = await repository.findOne({ where: { username: createUserRequest.username, }, }); expect(user).toMatchObject({ username: createUserRequest.username, }); }); }); });
Ā 
We have lots of new information just in this first part, right? letā€™s break it down in a few parts in order to understand the concepts.

Base

let app: any; let httpServer: any; let repository: Repository<Users>; let authService: AuthService; beforeAll(async () => { const module = await Test.createTestingModule({ imports: [DatabaseModule, AuthModule], }).compile(); app = module.createNestApplication(); authService = module.get<AuthService>(AuthService); repository = module.get<Repository<Users>>(getRepositoryToken(Users)); await app.init(); httpServer = app.getHttpServer(); }); afterAll(async () => { await app.close(); });
Ā 
I am calling this part of ā€œbaseā€, however, it can change according to the test that you are going to apply, OK?
beforeAll is a function that is going to run every time before every test inside this file, you can take advantage here of pre-defined mocks, connections, and everything that you will need to run it.
afterAll is almost the same concept as beforeAll, but, instead of running at the beginning, it will run at the end and in this case, we here are just closing the app instance.
Going deep into the beforeAll, we have the definition of the module (this one basically you will always have inside a test, however, its content will change according to what it's going to test, in our example, we are testing the application focus on AuthContollers, so I am focusing this test file with all the dependencies that the AuthModule can have, thatā€™s why I am importing DatabaseModule besides AuthModule indeed.
Both authService and repository are variables that are getting the service and repository instance that were previously called inside the createTestingModule, they will be used inside of the tests to create and check if some behaviors really happened inside of the system by the interaction with the endpoints.
Lastly, httpServer = app.getHttpServer(); is the URL of the test application, it will be used with Supertest to send requests to the endpoints.
Ā 

Keeping the base clean

Have in mind that we are using the APIs inside of one same Postgres instance, so in this case according to our tests are running, it can turns out being dirty because of the tests
Ā 
There are several approaches to keep the test database clean, in this example, I am defining one username fixed and between each test, I am cleaning the record in order to keep everything as neat as possible, for example.
const testUsername = 'authControllerE2ETests'; afterEach(async () => { await repository.delete({ username: testUsername, }); });
Ā 
In this case, I am using the repository that I mentioned previously to interact with the database directly and keep it clear.
Ā 

SingUp

Letā€™s break this first test into two parts, the first one is testing the usage of the endpoint. It is clear to read what is happening here, look
const createUserRequest: { username: string; password: string } = { username: testUsername, password: 'password', }; const response = await request(httpServer) .post('/auth/signUp') .send(createUserRequest); expect(response.status).toBe(HttpStatus.OK); expect(response.body).toHaveProperty( 'username', createUserRequest.username, ); expect(response.body).toHaveProperty('id');
Ā 
We are basically, using the endpoint and sending the data as it was defined, and using the response we are checking the values with the expect methods.
In the second part, we are making sure the value was saved adequately inside the database. Here again, we are using the repository to access the database and check the record.
const user = await repository.findOne({ where: { username: createUserRequest.username, }, }); expect(user).toMatchObject({ username: createUserRequest.username, });
Ā 
To test it, you need to run
npm run test:e2e
Ā 
I hope you can see something like this šŸ˜…
notion image
Ā 

Ā 

SignIn

describe('signIn', () => { it('should create the JWT', async () => { const createUserRequest = { username: testUsername, password: 'password', }; const newUser = repository.create(createUserRequest); await repository.save(newUser); const signInRequest = { username: testUsername, password: 'password', }; const signInResponse = await request(httpServer) .post('/auth/signIn') .send(signInRequest); expect(signInResponse.status).toBe(HttpStatus.OK); expect(signInResponse.body).toHaveProperty('access_token'); }); });
Ā 
The case here is similar, however, we are no longer testing the SignUp endpoint, instead, we are using the repository to create the user record inside the database and then we are using the SignIn endpoint to make the sign-in and finally, checking the response properly.
We are doing the test like that, because we tested the Sign up previously, and we want to keep the segregation of test responsibilities, and the responsibility of this test now is to check if the JWT is being created when the endpoint is used.
Ā 

Get profile data

describe('getProfile', () => { it('should get the user profile with valid JWT', async () => { const createUserRequest = { username: testUsername, password: 'password', }; const newUser = repository.create(createUserRequest); await repository.save(newUser); const signInResponse = await authService.signIn( createUserRequest.username, createUserRequest.password, ); expect(signInResponse).toHaveProperty('access_token'); const { access_token } = signInResponse; const profileResponse = await request(httpServer) .get('/auth/profile') .set('Authorization', `Bearer ${access_token}`); expect(profileResponse.status).toBe(HttpStatus.OK); expect(profileResponse.body).toHaveProperty( 'username', createUserRequest.username, ); }); it('should not get the user profile without a valid JWT', async () => { const profileResponse = await request(httpServer).get('/auth/profile'); expect(profileResponse.status).toBe(HttpStatus.UNAUTHORIZED); }); });
Ā 
For this last case, we are testing if after the user creates his account, he can use the endpoint to get his data, and respecting the guards and the usage of JWT inside of the endpoint.
As you can see, we have two tests, one to test the ā€œhappy caseā€ and the second one to test the ā€œbad caseā€ when the user is doing something wrong.
Ā 

Configuring the environments

The main idea is to run the tests inside a CI/CD pipeline, for this reason, we need to adapt the way that our system is getting the running environments, in this case, I am going to get the code related to the module env from the other post that we did.
Ā 
As we are going to have a new module, It is necessary to add it inside of the app.module.ts
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; import { UsersModule } from './users/users.module'; import { DatabaseModule } from './database/database.module'; import { EnvModule } from './env/env.module'; @Module({ imports: [EnvModule, AuthModule, UsersModule, DatabaseModule], controllers: [AppController], providers: [AppService], }) export class AppModule {}
Ā 
Now, our database.module.ts also needs to be improved
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { EnvModule } from '../env/env.module'; import { EnvService } from '../env/env.service'; @Module({ imports: [ TypeOrmModule.forRootAsync({ imports: [EnvModule], inject: [EnvService], useFactory(env: EnvService) { const isTesting = env.get('NODE_ENV') === 'test'; return { type: 'postgres', host: 'localhost', port: 5432, username: 'postgres', password: 'root', database: isTesting ? 'tests' : 'project', entities: [`${__dirname}/../**/*.entity{.ts,.js}`], migrations: [`${__dirname}/../migrations/*{.ts,.js}`], synchronize: false, migrationsRun: true, logging: true, }; }, }), ], }) export class DatabaseModule {}
Ā 
Thereā€™s something important here, look at the conditional
database: isTesting ? 'tests' : 'project',
Ā 
It means that when the test is running, it will use a database called tests. By default, jest will put the NODE_ENV with the value test and then this conditional will be true.
Ā 
As we importanted the code from the other project, donā€™t forget to install the two necessery dependencies, zod and @nestjs/config
Ā 
As we made some changes to how the software works, we also had to update the test dependencies
const module = await Test.createTestingModule({ imports: [EnvModule, DatabaseModule, AuthModule], }).compile();
Ā 

Adding Github actions to run the e2e

To finish our post, letā€™s add something else very cool and useful, letā€™s add the tests to run inside our CI/CD pipeline, it will help the project to guarantee that everybody is respecting the tests. Letā€™s create a file inside .github/workflows/ci.yml
name: Run integration testing on: push: branches: [main] pull_request: branches: [main] env: NODE_VERSION: 19.0.1 jobs: integration-test: runs-on: ubuntu-latest services: postgres: image: postgres:15-alpine env: POSTGRES_USER: postgres POSTGRES_PASSWORD: root POSTGRES_DB: tests ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout repository uses: actions/checkout@v2 - name: Use Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} - name: Install Dependencies run: yarn install - name: Integration testing run: | yarn test:e2e env: NODE_ENV: test
Ā 
This configuration looks like a ā€œcake recipeā€, where we are setting all steps, look at the beginning where we are saying that we need a container of Postgres with a specific configuration
services: postgres: image: postgres:15-alpine env: POSTGRES_USER: postgres POSTGRES_PASSWORD: root POSTGRES_DB: tests ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
Ā 
Then, we are saying to install the dependencies and run the e2e
steps: - name: Checkout repository uses: actions/checkout@v2 - name: Use Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} - name: Install Dependencies run: yarn install - name: Integration testing run: | yarn test:e2e env: NODE_ENV: test
Ā 
Our service is ready to understand the NODE_ENV with the value test and set the test environments according to what we need.
notion image

Conclusion

In the end, I hope you can see something like this.
notion image
Ā 
I always feel so good about myself in seeing all green šŸ˜….
We did here a basic example of e2e tests for our auth controller, but remember, it is important to guarantee the essential use cases of your system, just to make sure any changes that can have been implemented do not cause a bad experience to users. Even though we donā€™t want to create bugs, some things happen eventually.
Ā 

Codebase

nestjs-auth-flow-blog-post-with-e2e
nestjsninja ā€¢ Updated Jan 5, 2024
Ā