Ausnahme gefangen: SSL certificate problem: certificate is not yet valid 📌 NestJS - Unit and E2E testing

🏠 Team IT Security News

TSecurity.de ist eine Online-Plattform, die sich auf die Bereitstellung von Informationen,alle 15 Minuten neuste Nachrichten, Bildungsressourcen und Dienstleistungen rund um das Thema IT-Sicherheit spezialisiert hat.
Ob es sich um aktuelle Nachrichten, Fachartikel, Blogbeiträge, Webinare, Tutorials, oder Tipps & Tricks handelt, TSecurity.de bietet seinen Nutzern einen umfassenden Überblick über die wichtigsten Aspekte der IT-Sicherheit in einer sich ständig verändernden digitalen Welt.

16.12.2023 - TIP: Wer den Cookie Consent Banner akzeptiert, kann z.B. von Englisch nach Deutsch übersetzen, erst Englisch auswählen dann wieder Deutsch!

Google Android Playstore Download Button für Team IT Security



📚 NestJS - Unit and E2E testing


💡 Newskategorie: Programmierung
🔗 Quelle: dev.to

Testing is a fundamental aspect of development that helps build trust in the code and ensures it can handle various scenarios. It also serves as an early warning system when changes are made to the code. If a test fails, it indicates that an unintended change has occurred, which either requires fixing the code or updating the test.

In this post, we take a look at implementing testing in a NestJS application which was set up previously in this post - NestJS – Supercharging Node.js Applications. Now that we have a basic understanding of NestJS, this post will explore how to test the API.

Test Types

There are three main recommendations for testing:
Unit testing
End-to-end (E2E) testing
SuperTest

Unit testing
It is best for a JavaScript function to be kept short, ideally with no more than three lines of code. However, this can be difficult to adhere to. Unit testing involves testing a function on its own, to ensure it is isolated and functioning properly. This allows for thorough testing of each part of the code, independently, and without interference from other functions or services. It is highly recommended to have as many tests as possible, as there is no such thing as too many tests.

E2E testing
As mentioned in my first blog, NestJS has a designated entry point module. In the example provided, the TodoModule was the entry point, responsible for setting up routes, injecting controllers, and services. End-to-end (E2E) testing can be used to verify the functionality of this module. The test bed injects the TodoModule and sets up routes, controllers, and services. By testing the module end-to-end, we can ensure that it works as intended throughout the entire process.

Supertest
We need a solution that can test the API regardless of the framework it is built on. The API can be built using NodeJS, NestJS, or any other framework. Supertest provides a framework-agnostic testing suite that allows for end-to-end testing of the API.

Configuration

To configure the tests, let us start by installing some dependencies.
$ yarn add supertest jest-sonar jest-junit @jest-performance-reporter/core --save-dev

Now, let’s set up an initial base configuration.

jest.config.js
This is the base configuration for all the tests.

module.exports = {
 testEnvironment: 'node',
 preset: 'ts-jest',
 rootDir: './',
 modulePaths: ['<rootDir>'],
 moduleNameMapper: {
   '^src$': '<rootDir>/src',
   '^src/(.+)$': '<rootDir>/src/$1',
 },
 modulePathIgnorePatterns: ['src/typings'],
 testPathIgnorePatterns: [
   '/node_modules./',
   '<rootDir>/(coverage|dist|lib|tmp)./',
 ],
};

jest.unit.js
This is the configuration for unit tests, which inherits the base configuration. The coverage has been set to low for the time being. Ideally, this has to be above 80.

const sharedConfig = require('./jest.config');

module.exports = {
 ...sharedConfig,
 coverageDirectory: 'coverage',
 coverageReporters: ['text', 'lcov', 'cobertura'],
 collectCoverageFrom: [
   'src/**/*.ts',
   '!*/node_modules/**',
   '!<rootDir>/src/main.ts',
   '!<rootDir>/src/modules/database/database.service.ts',
 ],
 reporters: [
   'default',
   'jest-sonar',
   [
     'jest-junit',
     {
       outputDirectory: 'junit',
       outputName: 'test-results.xml',
     },
   ],
   [
     '@jest-performance-reporter/core',
     {
       errorAfterMs: 1000,
       warnAfterMs: 500,
       logLevel: 'warn',
       maxItems: 5,
       jsonReportPath: 'performance-report.json',
       csvReportPath: 'performance-report.csv',
     },
   ],
 ],
 coverageThreshold: {
   global: {
     branches: 10,
     functions: 10,
     lines: 10,
     statements: 10,
   },
 },
};

jest.e2e.js
This is the configuration for E2E tests, which inherits the base configuration.

const sharedConfig = require('./jest.config');

module.exports = {
 ...sharedConfig,
 moduleFileExtensions: ['js', 'json', 'ts'],
 testRegex: '.e2e-spec.ts$',
 transform: {
   '^.+\\.(t|j)s$': 'ts-jest',
 },
};

jest.supertest.js
This is the configuration for supertest tests, which inherits the base configuration.

const sharedConfig = require('./jest.config');

module.exports = {
 ...sharedConfig,
 moduleFileExtensions: ['js', 'json', 'ts'],
 testRegex: '.supertest-spec.ts$',
 transform: {
   '^.+\\.(t|j)s$': 'ts-jest',
 },
};

Now that we have all the configurations in place, we need to update our scripts in package.json. All/update the following scripts.

   "test": "jest --config ./jest.unit.js",
   "test:cov": "yarn test --coverage",
   "test:e2e": "jest --config ./jest.e2e.js",
   "test:supertest": "jest --config ./jest.supertest.js"

Testing

Now let’s look at our previous sample code repo and get our hands dirty with some testing. Before we start, let us mock the database service. We will look into implementing MongoDB as a separate blog.

import { Injectable } from '@nestjs/common';
import { ToDo } from 'src/models/ToDo';

@Injectable()
export class DataBaseService {
 data: ToDo[] = [
   {
     id: '1',
     description: 'Cook pasta',
     is_active: true,
     created_at: new Date(),
   },
   {
     id: '2',
     description: 'Do laundry',
     is_active: true,
     created_at: new Date(),
   },
   {
     id: '3',
     description: 'Clean kitchen',
     is_active: false,
     created_at: new Date(),
   },
 ];

 getAll(): Promise<ToDo[]> {
   return Promise.resolve(this.data);
 }

 get(id: string): Promise<ToDo> {
   return Promise.resolve(this.data.filter((t) => t.id === id)[0]);
 }

 create(todo: ToDo): Promise<ToDo> {
   this.data.push(todo);
   return Promise.resolve(todo);
 }

 update(todo: ToDo): Promise<ToDo> {
   const dataIndex = this.data.findIndex((t) => t.id === todo.id);
   this.data[dataIndex] = todo;
   return Promise.resolve(this.data[dataIndex]);
 }

 delete(id: string): Promise<boolean> {
   this.data = this.data.filter((t) => t.id !== id);
   return Promise.resolve(true);
 }
}

Unit testing
Before we start, let us run the tests and coverage so we can compare the before and after results.

$ yarn test:cov

Image description

Let us start by adding unit tests to the controller, service and module.

Unit testing service
This is an example of what unit tests for a service may look like. Although it may appear extensive for a small service, it has several benefits. Each function is tested individually and, in the event of a failure, the test can pinpoint the specific point of failure. The use of "describe" blocks isolates each test, providing a level of abstraction and assurance they are not affected by other neighbouring tests.

import { Test, TestingModule } from '@nestjs/testing';
import { ToDo } from 'src/models/ToDo';
import { DataBaseService } from 'src/modules/database/database.service';
import { TodoModule } from './todo.module';
import { TodoService } from './todo.service';

describe('TodoService', () => {
 let service: TodoService;

 const mockDataBaseService = {
   getAll: jest.fn(),
   get: jest.fn(),
   create: jest.fn(),
   update: jest.fn(),
   delete: jest.fn(),
 };

 beforeEach(async () => {
   const module: TestingModule = await Test.createTestingModule({
     imports: [TodoModule],
   })
     .overrideProvider(DataBaseService)
     .useValue(mockDataBaseService)
     .compile();

   service = module.get<TodoService>(TodoService);
 });

 it('should have the service defined', () => {
   expect(service).toBeDefined();
 });

 describe('#getAll', () => {
   beforeEach(() => {
     jest.spyOn(mockDataBaseService, 'getAll');
   });

   it('should be defined', () => {
     expect(service.getAll).toBeDefined();
   });

   it('should call the database', () => {
     service.getAll();
     expect(mockDataBaseService.getAll).toBeCalledTimes(1);
   });
 });

 describe('#get', () => {
   beforeEach(() => {
     jest.spyOn(mockDataBaseService, 'get');
   });

   it('should be defined', () => {
     expect(service.get).toBeDefined();
   });

   it('should call the database', () => {
     service.get('1');
     expect(mockDataBaseService.get).toBeCalledTimes(1);
   });
 });

 describe('#create', () => {
   beforeEach(() => {
     jest.spyOn(mockDataBaseService, 'create');
   });

   it('should be defined', () => {
     expect(service.create).toBeDefined();
   });

   it('should call the database', () => {
     service.create({} as ToDo);
     expect(mockDataBaseService.create).toBeCalledTimes(1);
   });
 });

 describe('#update', () => {
   beforeEach(() => {
     jest.spyOn(mockDataBaseService, 'update');
   });

   it('should be defined', () => {
     expect(service.update).toBeDefined();
   });

   it('should call the database', () => {
     service.update({} as ToDo);
     expect(mockDataBaseService.update).toBeCalledTimes(1);
   });
 });

 describe('#delete', () => {
   beforeEach(() => {
     jest.spyOn(mockDataBaseService, 'delete');
   });

   it('should be defined', () => {
     expect(service.delete).toBeDefined();
   });

   it('should call the database', () => {
     service.delete('1');
     expect(mockDataBaseService.delete).toBeCalledTimes(1);
   });
 });

 describe('#markAsInActive', () => {
   beforeEach(() => {
     jest.spyOn(mockDataBaseService, 'get');
     jest.spyOn(mockDataBaseService, 'update');
   });

   it('should be defined', () => {
     expect(service.markAsInActive).toBeDefined();
   });

   describe('when inactive is called', () => {
     it('should call the databaseService.get', () => {
       expect(mockDataBaseService.get).toBeCalledTimes(1);
     });

     it('should call the databaseService.update', () => {
       expect(mockDataBaseService.update).toBeCalledTimes(1);
     });
   });
 });
});

Unit testing controller
Unit tests for controllers are similar to a service. Let us have a closer look at the get function, specifically.

get(@Param() params: { id: string }): Promise<ToDo> {
   return this.todoService.get(params.id);
 }

This function is responsible for reading the id from params, and invoking and returning the get function from service. So in our unit test, we test exactly the same. We are making sure when this function in the controller is being invoked, the relevant function in service is being called once.

import { Test, TestingModule } from '@nestjs/testing';
import { ToDo } from 'src/models/ToDo';
import { TodoController } from './todo.controller';
import { TodoService } from './todo.service';

describe('TodoController', () => {
 let controller: TodoController;
 let service: TodoService;

 const mockTodoService = {
   getAll: jest.fn(),
   get: jest.fn(),
   create: jest.fn(),
   update: jest.fn(),
   delete: jest.fn(),
   markAsInActive: jest.fn(),
 };

 beforeEach(async () => {
   const module: TestingModule = await Test.createTestingModule({
     providers: [TodoService],
     controllers: [TodoController],
   })
     .overrideProvider(TodoService)
     .useValue(mockTodoService)
     .compile();

   controller = module.get<TodoController>(TodoController);
   service = module.get<TodoService>(TodoService);
 });

 it('should be defined', () => {
   expect(controller).toBeDefined();
 });

 describe('#getAll', () => {
   beforeEach(() => {
     jest.spyOn(service, 'getAll');
   });

   it('should be defined', () => {
     expect(service.getAll).toBeDefined();
   });

   it('should call service.getAll', () => {
     controller.getAll();
     expect(service.getAll).toBeCalledTimes(1);
   });
 });

 describe('#get', () => {
   beforeEach(() => {
     jest.spyOn(service, 'get');
   });

   it('should be defined', () => {
     expect(service.get).toBeDefined();
   });

   it('should call service.get', () => {
     controller.get({ id: '1' });
     expect(service.get).toBeCalledTimes(1);
   });
 });

 describe('#create', () => {
   beforeEach(() => {
     jest.spyOn(service, 'create');
   });

   it('should be defined', () => {
     expect(service.create).toBeDefined();
   });

   it('should call service.create', () => {
     controller.create({} as ToDo);
     expect(service.create).toBeCalledTimes(1);
   });
 });

 describe('#update', () => {
   beforeEach(() => {
     jest.spyOn(service, 'update');
   });

   it('should be defined', () => {
     expect(service.update).toBeDefined();
   });

   it('should call service.update', () => {
     controller.update({} as ToDo);
     expect(service.update).toBeCalledTimes(1);
   });
 });

 describe('#delete', () => {
   beforeEach(() => {
     jest.spyOn(service, 'delete');
   });

   it('should be defined', () => {
     expect(service.delete).toBeDefined();
   });

   it('should call service.delete', () => {
     controller.delete({ id: '1' });
     expect(service.delete).toBeCalledTimes(1);
   });
 });

 describe('#markAsInActive', () => {
   beforeEach(() => {
     jest.spyOn(service, 'markAsInActive');
   });

   it('should be defined', () => {
     expect(service.markAsInActive).toBeDefined();
   });

   it('should call service.markAsInActive', () => {
     controller.markAsInActive({ id: '1' });
     expect(service.markAsInActive).toBeCalledTimes(1);
   });
 });
});

Unit testing module
Unit testing modules are comparatively easier. The responsibility of this test is to make sure that the relevant controllers and services are injected.

import { Test, TestingModule } from '@nestjs/testing';

import { TodoController } from './Todo.controller';
import { TodoModule } from './Todo.module';
import { TodoService } from './Todo.service';

describe('TodoModule', () => {
 let module: TestingModule;

 beforeAll(async () => {
   module = await Test.createTestingModule({
     imports: [TodoModule],
   }).compile();
 });

 it('should compile the module', async () => {
   expect(module).toBeDefined();
 });

 it('should have Todo components', async () => {
   expect(module.get(TodoController)).toBeInstanceOf(TodoController);
   expect(module.get(TodoService)).toBeInstanceOf(TodoService);
 });
});

Now that we are finished with unit testing, let us run the coverage and compare the results. We can see that the coverage for statements, branches, functions and lines have increased significantly. We can talk a bit more about the coverage and jest setting at the end.

Image description

This coverage report shows us a clear picture of the coverage of tests on each file. The most important column is that labelled ‘Uncovered Lines’. You’ll see that, for example, todo.service.ts has uncovered lines of code from 32 to 34. We now have to write unit tests to make sure those lines are also covered. In certain scenarios, it might not be possible to get 100% coverage. The goal is to get maximum coverage, and 80% or higher is considered good. If we can achieve 90% or more, we have a very confident test suite. However, it’s worth noting that we should not achieve these results by excluding files in the config.

E2E testing

NestJS E2E testing involves testing the application end-to-end, starting from main.ts all the way to service. This is an easy way to test whether all the components are wired up correctly. This ensures the modules and its injections are in order, all the dependency injections are correct in controller and service, and confirms the routing is done as expected.

E2E testing Todo Module
E2E tests are written for each module. We have to make sure that we hit all the controller endpoints, which are technically all the routes configured through a module. So, now we are going to test the Todo Module end-to-end. We will be mocking the DatabaseService for the time being. Similar to the NestJS functions, we don’t need to test the database, as it is implied it works as expected.

import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';

import { TodoService } from 'src/modules/todo/todo.service';
import { AppModule } from 'src/app.module';

describe('TodoModule', () => {
 let app: INestApplication;

 const mockTodoService = {
   getAll: jest.fn(),
   get: jest.fn(),
   create: jest.fn(),
   update: jest.fn(),
   delete: jest.fn(),
   markAsInActive: jest.fn(),
 };

 beforeEach(async () => {
   const moduleFixture: TestingModule = await Test.createTestingModule({
     imports: [AppModule],
   })
     .overrideProvider(TodoService)
     .useValue(mockTodoService)
     .compile();

   app = moduleFixture.createNestApplication();
   await app.init();
 });

 afterEach(() => {
   jest.clearAllMocks();
 });

 describe('GET: todo/:id', () => {
   beforeEach(() => {
     jest.spyOn(mockTodoService, 'get');
   });

   it('should return OK', async () => {
     await request(app.getHttpServer()).get('/todo/1').expect(200, {});
   });
 });

 describe('GET: todo/all', () => {
   beforeEach(() => {
     jest.spyOn(mockTodoService, 'getAll');
   });

   it('should return OK', async () => {
     await request(app.getHttpServer()).get('/todo/all').expect(200, {});
   });
 });

 describe('POST: todo', () => {
   beforeEach(() => {
     jest.spyOn(mockTodoService, 'create');
   });

   it('should return OK', async () => {
     await request(app.getHttpServer()).post('/todo').expect(201, {});
   });
 });

 describe('PUT: todo', () => {
   beforeEach(() => {
     jest.spyOn(mockTodoService, 'update');
   });

   it('should return OK', async () => {
     await request(app.getHttpServer()).put('/todo').expect(200, {});
   });
 });

 describe('PUT: todo/inactive/:id', () => {
   beforeEach(() => {
     jest.spyOn(mockTodoService, 'update');
   });

   it('should return OK', async () => {
     await request(app.getHttpServer())
       .put('/todo/inactive/:id')
       .expect(200, {});
   });
 });

 describe('DELETE: todo/:id', () => {
   beforeEach(() => {
     jest.spyOn(mockTodoService, 'delete');
   });

   it('should return OK', async () => {
     await request(app.getHttpServer()).delete('/todo/:id').expect(200, {});
   });
 });
});

Now we can run the E2E tests.
$ yarn test:e2e

Image description

Super test

In the above E2E test set up, we can see that we are injecting the test bed with the modules. The E2E is not agnostic of NestJS and its components. We might have to override the components and services to make sure that the test can pass.

Now we need a test set up that is agnostic of the language or framework we have used. So we will run the application and our test will run against the running application. These tests can also be used to run against different environments. Since this test is framework agnostic, it will come handy if we decide to change the framework, for instance from NestJS to C#. This can also help us avoid regressions as it can be run against multiple environments.

import * as request from 'supertest';

const baseURL = 'http://localhost:3000/';

describe('Todo', () => {
 const apiRequest = request(baseURL);

 describe('GET: todo/:id', () => {
   it('should have the response', async () => {
     const response = await apiRequest.get('todo/1');

     expect(response.status).toBe(200);
   });
 });

 describe('GET: todo/all', () => {
   it('should have the response', async () => {
     const response = await apiRequest.get('todo/all');

     expect(response.status).toBe(200);
   });
 });

 describe('POST: todo', () => {
   it('should have the response', async () => {
     const response = await apiRequest.post('todo').send({});

     expect(response.status).toBe(201);
   });
 });

 describe('PUT: todo', () => {
   it('should have the response', async () => {
     const response = await apiRequest.put('todo').send({});

     expect(response.status).toBe(200);
   });
 });

 describe('PUT: todo/inactive/:id', () => {
   it('should have the response', async () => {
     const response = await apiRequest.put('todo').send({});

     expect(response.status).toBe(200);
   });
 });

 describe('DELETE: todo/:id', () => {
   it('should have the response', async () => {
     const response = await apiRequest.delete('todo/1');

     expect(response.status).toBe(200);
   });
 });
});

Before we run the tests, we have to run the application. This is because this test will be hitting the actual endpoints of the application rather than the code itself. Note that in cases of authenticated endpoints, we might have to pass header or token so that the test can pass.

$ yarn test:supertest

Image description

There are a few things to keep an eye on here. Firstly, we are only checking the status of the response, however we can and should also check the response in here, which increases the reliability of the test.

You can also configure a helper which gives you a response to compare against while running on different environments. I have hardcoded the base URL here for the ease of the demo. Ideally, we need the base URL to be read from an environment config file.

Conclusion

Now that we have three sets of tests, we have achieved a higher level of confidence in updating existing implementations and introducing more features. These tests will help us in multiple ways. Firstly, they act like a technical documentation of what the expected behaviour is. Secondly, this will help us in alerting any unwanted bugs or regression that we might have been accidentally introduced. These tests should be added to the CI/CD pipelines so they can run against every change. The supertests can be very handy if we set them to run against all environments, if possible.

In upcoming blogs, we will explore how these tests can become even more helpful when we start to add new features to this existing code base.

Code: https://github.com/rohithart/nestjs-todo
NestJS Documentation: https://docs.nestjs.com/

...



📌 Queuing jobs in NestJS using @nestjs/bullmq package


📈 41 Punkte

📌 What Is End-To-End Testing? E2E Testing Tutorial With Examples and Best Practices


📈 37.16 Punkte

📌 What Is End-To-End Testing? E2E Testing Tutorial With Examples and Best Practices


📈 37.16 Punkte

📌 E2E-Testing mit Playwright: Der Weg der Mitte


📈 27.94 Punkte

📌 E2E Testing using TestCafe


📈 27.94 Punkte

📌 E2E Testing using TestCafe


📈 27.94 Punkte

📌 Make E2e Testing Easier With the Right Tools


📈 27.94 Punkte

📌 e2e testing with Playwright | OD111


📈 27.94 Punkte

📌 API Security Assurance via E2E Testing - Alex Mor


📈 27.94 Punkte

📌 Scalable REST APIs with NestJS: A Testing-Driven Approach


📈 27.94 Punkte

📌 spectest - API testing library for Go that generate E2E test result document in markdown


📈 27.94 Punkte

📌 WebAuthn E2E Testing: Playwright, Selenium, Puppeteer


📈 27.94 Punkte

📌 Local Development with AWS Lambda and NestJS: Docker Debugging and Hot Reload


📈 24.07 Punkte

📌 Learn How to Create and Test a File Upload API using NestJS and Postman


📈 24.07 Punkte

📌 Performance Testing vs. Load Testing vs. Stress Testing


📈 22.32 Punkte

📌 NestJS Authentication with OAuth2.0: Configuration and Operations


📈 22.28 Punkte

📌 Modernizing Express, Heroku tech stack with NestJS and AWS - PoC


📈 22.28 Punkte

📌 Step-by-Step Guide: Setting Up a NestJS Application with Docker and PostgreSQL


📈 22.28 Punkte

📌 [Nestia] Boost up your NestJS server much faster and easier (maximum 20,000x faster)


📈 22.28 Punkte

📌 [Nestia] Boost up your NestJS server much faster and easier (maximum 20,000x faster)


📈 22.28 Punkte

📌 Integrate MongoDB database with multiple collections using Mongoose in NestJS and Typescript


📈 22.28 Punkte

📌 Error Handling and Logging in NestJS: Best Practices


📈 22.28 Punkte

📌 Harnessing the Power of a Monorepo: AWS Serverless API Gateway, NestJS, and Microservices gRPC


📈 22.28 Punkte

📌 Hasura and Keycloak integration with NestJS server


📈 22.28 Punkte

📌 Hasura and Keycloak integration with NestJS server


📈 22.28 Punkte

📌 Part 2: Setup Dashboard with grafana and nestjs


📈 22.28 Punkte

📌 NestJS Emails with react-email and nodemailer


📈 22.28 Punkte

📌 How to Build a CRUD REST API with NestJS, Docker, Swagger, and Prisma


📈 22.28 Punkte

📌 Managing Next.js and NestJS Applications in Production with PM2


📈 22.28 Punkte

📌 Dockerizing a NestJS app and persisting data


📈 22.28 Punkte

📌 Mastering NestJS: Unleashing the Power of Clean Architecture and DDD in E-commerce Development — part 1


📈 22.28 Punkte

📌 Text translation using langchain.js and Gemini in a NestJS application


📈 22.28 Punkte

📌 MongoDB and PostgreSQL Support for NestJS Boilerplate


📈 22.28 Punkte

📌 Mastering Session Management with NestJS and Redis: A Comprehensive Guide


📈 22.28 Punkte

📌 Cloupia Framework E2E - Directory Traversal Vulnerability


📈 20.5 Punkte











matomo