Testing NestJS Applications
Writing unit tests, integration tests, and end-to-end tests using Jest and Supertest.
Setting Up the Testing Environment with Jest and Supertest in NestJS
This guide will walk you through setting up a robust testing environment for your NestJS applications using Jest and Supertest. We'll cover installing dependencies, configuring Jest, and understanding the basics of Supertest for making HTTP requests within your tests.
1. Installing Necessary Dependencies
First, you need to install the required packages using npm or yarn. These include Jest, Supertest, and their related TypeScript typings.
npm install --save-dev jest supertest @types/jest @types/supertest ts-jest jest-mock-extended
Alternatively, using yarn:
yarn add -D jest supertest @types/jest @types/supertest ts-jest jest-mock-extended
Here's a breakdown of the packages:
jest
: JavaScript testing framework.supertest
: HTTP assertions made easy. Allows you to make HTTP requests to your application within your tests.@types/jest
: TypeScript definitions for Jest.@types/supertest
: TypeScript definitions for Supertest.ts-jest
: A preprocessor that allows Jest to run TypeScript code. Transforms your TypeScript code into JavaScript before running tests.jest-mock-extended
: Provides strongly-typed mocks for Jest, enhancing type safety and preventing common mocking errors.
2. Configuring Jest
Next, you need to configure Jest. Create a jest.config.js
file in the root of your project. Here's a basic configuration:
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: [
'**/*.(t|j)s',
],
coverageDirectory: '../coverage',
testEnvironment: 'node',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', // Optional: If you use path aliases in your code.
},
};
Let's explain the important options:
moduleFileExtensions
: Tells Jest which file extensions to look for.rootDir
: The root directory to scan for tests.testRegex
: A regular expression that matches the test files. In this case, it's looking for files ending in.spec.ts
.transform
: Specifies how to transform your code before running tests. Here, we're usingts-jest
to transform TypeScript into JavaScript.collectCoverageFrom
: Specifies which files to collect coverage information from.coverageDirectory
: The directory where coverage reports will be placed.testEnvironment
: Sets the testing environment to Node.js.moduleNameMapper
: (Optional) If you use path aliases in your code (e.g.,@/modules/users
), you need to tell Jest how to resolve them. This example maps any import starting with@/
to thesrc
directory. Adjust this based on your project's structure.
Important: You may need to adjust the testRegex
and moduleNameMapper
to match your project's file structure and path aliases.
3. Integrating Jest with NestJS
You'll typically add a script to your package.json
to run your tests:
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
}
Here's what each script does:
test
: Runs all tests once.test:watch
: Runs tests in watch mode, re-running tests whenever files change.test:cov
: Runs tests and generates a coverage report.test:debug
: Allows you to debug your tests using a debugger.
4. Understanding Supertest
Supertest allows you to make HTTP requests to your NestJS application within your tests. This is crucial for testing your controllers and API endpoints.
Here's a basic example of how to use Supertest in a NestJS test:
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module'; // Replace with your actual AppModule path
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
Let's break down the code:
- We import necessary modules from
@nestjs/testing
andsupertest
. - We create a
TestingModule
usingTest.createTestingModule
, importing yourAppModule
(or the specific module you want to test). - We create a NestJS application instance using
moduleFixture.createNestApplication()
and initialize it. - We use
request(app.getHttpServer())
to create a Supertest agent that can make requests to your application's HTTP server. - We use methods like
.get()
,.post()
,.put()
,.delete()
to make HTTP requests. - We use
.expect()
to assert the expected status code and response body.
5. Writing Your First Test
Create a file named app.controller.spec.ts
(or a similar name) in your src
directory (or a test
directory if you prefer). Copy the example code from above into this file. Adjust the AppModule
path to match your project.
Now, run your tests using npm run test
or yarn test
. You should see your test pass.
6. Testing Asynchronously
Often your NestJS endpoints will be asynchronous. Supertest handles this gracefully.
it('/async (GET)', async () => {
const response = await request(app.getHttpServer())
.get('/async'); // Assuming you have an endpoint '/async'
expect(response.status).toBe(200);
expect(response.body).toEqual({ message: 'Async Response' });
});
Key points:
- The test function is declared
async
. - We
await
the result of the Supertest request. This ensures the test waits for the asynchronous operation to complete before making assertions.
7. Mocking Services and Dependencies
When testing controllers, you often want to isolate them and mock their dependencies (e.g., services). Here's how you can do that:
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
describe('AppController', () => {
let appController: AppController;
let appService: AppService;
let app: INestApplication;
beforeEach(async () => {
const mockAppService = {
getHello: jest.fn().mockReturnValue('Mocked Hello World!'), // Mock the getHello method
};
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [{ provide: AppService, useValue: mockAppService }],
}).compile();
appController = app.get<AppController>(AppController);
appService = app.get<AppService>(AppService);
app = app.createNestApplication();
await app.init();
});
describe('root', () => {
it('should return "Mocked Hello World!"', async () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Mocked Hello World!');
});
});
});
Key concepts:
- We create a mock implementation of the
AppService
usingjest.fn()
. - We provide this mock implementation to the
Test.createTestingModule
using theproviders
array and theuseValue
property. This tells NestJS to use our mock service instead of the real one when creating the controller. - In the test, the
getHello()
method of theAppService
will now return "Mocked Hello World!" instead of the actual implementation.
8. Using `jest-mock-extended`
For more robust and type-safe mocking, consider using the `jest-mock-extended` library.
npm install --save-dev jest-mock-extended
yarn add -D jest-mock-extended
Example using `jest-mock-extended`:
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { mock } from 'jest-mock-extended';
describe('AppController', () => {
let appController: AppController;
let appService: AppService;
let app: INestApplication;
beforeEach(async () => {
const mockAppService = mock<AppService>(); // Creates a typed mock of AppService
mockAppService.getHello.mockReturnValue('Mocked Hello World!'); // Sets up mock behavior
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [{ provide: AppService, useValue: mockAppService }],
}).compile();
appController = app.get<AppController>(AppController);
appService = app.get<AppService>(AppService);
app = app.createNestApplication();
await app.init();
});
describe('root', () => {
it('should return "Mocked Hello World!"', async () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Mocked Hello World!');
});
});
});
Key improvements:
- Type safety: `mock<AppService>()` creates a mock object that conforms to the `AppService` interface. This helps prevent errors when calling methods on the mock object.
- Clearer mocking syntax: The `mockAppService.getHello.mockReturnValue()` syntax makes it clear which method is being mocked and what value it should return.
9. Testing Specific Modules
For more complex applications, you might want to test modules in isolation rather than testing the entire application.
import { Test, TestingModule } from '@nestjs/testing';
import { UsersModule } from '../src/users/users.module'; // Replace with your module path
import { UsersController } from '../src/users/users.controller';
import { UsersService } from '../src/users/users.service';
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
describe('UsersModule (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [UsersModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/users (GET)', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect([]); // Assuming the default response is an empty array
});
});
By importing UsersModule
directly, you only bootstrap that module and its dependencies, making your tests faster and more focused.
10. Conclusion
By following these steps, you can set up a powerful and effective testing environment for your NestJS applications using Jest and Supertest. Remember to adapt the configurations and examples to match your specific project structure and requirements. Happy testing!