End-to-End (E2E) testing is a critical part of testing in any application. It ensures that the entire system works together as expected, from the frontend all the way to the backend, including all layers of your application. NestJS, with the help of the @nestjs/testing
module and Jest, makes it easy to implement effective E2E tests.
In this module, we will dive into End-to-End testing using NestJS, where we’ll learn how to set up an E2E testing environment, test the flow of data across various layers, and ensure that the entire application behaves as expected in a production-like environment.
Table of Contents
- Introduction to End-to-End Testing
- Setting Up the Testing Environment
- Writing E2E Tests in NestJS
- Using Supertest for HTTP Assertions
- Testing Full API Workflow
- Mocking and Handling Dependencies
- Best Practices for E2E Testing in NestJS
- Conclusion
Introduction to End-to-End Testing
End-to-End tests simulate real user interactions and verify that the entire system works as expected. E2E testing goes beyond unit and integration tests by verifying that the entire flow—from database interactions to HTTP responses—functions correctly.
For NestJS applications, E2E tests typically focus on testing controllers, services, and other components as a whole, by making HTTP requests, validating responses, and ensuring that the system behaves like it would in production.
Setting Up the Testing Environment
To get started with E2E testing in NestJS, you’ll need to set up a dedicated environment for running tests. NestJS uses Jest and Supertest to make this process seamless.
Install Dependencies
If you haven’t already, install the necessary dependencies:
bashCopyEditnpm install --save-dev jest @nestjs/testing ts-jest @types/jest supertest
Add or update the jest.config.js
file to include E2E settings:
jsCopyEditmodule.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
globalSetup: './test/setup.ts',
testTimeout: 30000, // Ensure the timeout is long enough for E2E tests
};
Writing E2E Tests in NestJS
NestJS integrates Jest with its testing utilities to allow you to write comprehensive E2E tests. The E2E tests can be written using @nestjs/testing
, and tests are typically located in the test
folder.
Example: Writing a Basic E2E Test
To begin, let’s test an endpoint in the app. Suppose you have a simple UsersController
with a GET /users
route that fetches user data.
Here’s how you can write a simple E2E test for it:
tsCopyEditimport { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('UsersController (E2E)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('should return a list of users (GET /users)', async () => {
const response = await request(app.getHttpServer()).get('/users');
expect(response.status).toBe(200);
expect(response.body).toEqual(expect.arrayContaining([expect.objectContaining({ id: expect.any(String), name: expect.any(String) })]));
});
afterAll(async () => {
await app.close();
});
});
Key Steps:
- Setup: The
beforeAll
hook initializes theINestApplication
instance using theAppModule
. - Test: The
it
block makes an HTTP request using Supertest and checks the response’s status and structure. - Teardown: The
afterAll
hook ensures that the NestJS application is closed after the test.
In the example above, we send a GET
request to the /users
route and verify that the response contains an array of users.
Using Supertest for HTTP Assertions
Supertest is a great utility for performing HTTP assertions in Node.js applications, and it works perfectly for E2E testing in NestJS. It allows you to simulate HTTP requests and check if the application behaves correctly under different conditions.
Example: POST Request with Body Validation
Let’s extend our E2E test by testing a POST /users
route that creates a new user.
tsCopyEditit('should create a new user (POST /users)', async () => {
const newUser = { name: 'John Doe', age: 30 };
const response = await request(app.getHttpServer())
.post('/users')
.send(newUser)
.expect(201);
expect(response.body.name).toBe(newUser.name);
expect(response.body.age).toBe(newUser.age);
expect(response.body).toHaveProperty('id');
});
Here, we simulate a POST
request with a newUser
payload and verify that the server returns a status of 201
and includes the correct user data in the response.
Testing Full API Workflow
One of the primary use cases for E2E tests is testing the full API workflow, including making sure that controllers, services, and repositories work together as expected.
Example: Full API Workflow Test
In this example, we’ll simulate the flow where the user data is sent to a service, saved in the database, and returned via the controller.
tsCopyEditit('should create and return a user (full workflow)', async () => {
const newUser = { name: 'Jane Doe', age: 25 };
// Create a new user
const createResponse = await request(app.getHttpServer())
.post('/users')
.send(newUser)
.expect(201);
const createdUserId = createResponse.body.id;
// Fetch the user
const getResponse = await request(app.getHttpServer())
.get(`/users/${createdUserId}`)
.expect(200);
expect(getResponse.body.name).toBe(newUser.name);
expect(getResponse.body.age).toBe(newUser.age);
expect(getResponse.body.id).toBe(createdUserId);
});
Explanation:
- Create User: We first send a
POST
request to create a user. - Fetch User: Then, we use the
GET
endpoint to fetch the user by their ID. - Assertions: Finally, we assert that the user data returned matches what we created.
This type of test ensures that the entire flow—from creating to fetching the user—works correctly.
Mocking and Handling Dependencies
In some cases, it may be necessary to mock certain dependencies during E2E tests, especially if those dependencies interact with external services like databases, file systems, or third-party APIs.
Example: Mocking a Service
Suppose we have a service that interacts with an external payment API. During E2E testing, you might want to mock the payment gateway to avoid real transactions.
tsCopyEditconst mockPaymentService = {
processPayment: jest.fn().mockResolvedValue({ status: 'success' }),
};
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
providers: [
{
provide: PaymentService,
useValue: mockPaymentService,
},
],
}).compile();
In this case, we use Jest’s jest.fn()
to mock the processPayment
method of the PaymentService
during testing.
Best Practices for E2E Testing in NestJS
- Isolate Tests: Always ensure that your tests are isolated from production data. Use in-memory databases or mock services where possible.
- Test Realistic Use Cases: Ensure that the E2E tests simulate real user interactions and cover the full workflow of your application.
- Clean Up After Tests: Make sure to clean up any test data created during the tests, especially when using real databases.
- Use CI/CD Pipelines: Automate your E2E tests by running them in a Continuous Integration pipeline to catch errors early in the development cycle.
Conclusion
In this module, we explored how to perform End-to-End (E2E) testing in NestJS using Jest and Supertest. We covered how to set up a testing environment, write tests for controllers and services, and simulate real HTTP requests to verify the full API workflow. By writing comprehensive E2E tests, you can ensure that your entire application works correctly, providing a seamless experience for end users.
E2E testing is crucial for identifying issues that might not be visible in unit or integration tests, ensuring that all components of your system work together as intended.