End-to-End Testing
Boot the whole app in a test, drive it with supertest, and override just the providers that touch the outside world.
Unit tests prove a class works in isolation; e2e tests prove the pieces work together - real routing, guards, pipes, and serialization. Nest makes this remarkably clean: you build the actual application and fire HTTP requests at it in-memory with supertest.
Boot the real app
Use Test.createTestingModule({ imports: [AppModule] }), call createNestApplication(), and init(). Apply the same global pipes and filters you use in main.ts so the test exercises production behavior, not a stripped-down variant.
const app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init();
await request(app.getHttpServer())
.get('/users')
.expect(200);Override only the edges
You want the real pipeline but not a real payment gateway or a real email send. overrideProvider() swaps those boundary providers for fakes while everything else runs for real. Keep the database real-ish with a test container or an in-memory SQLite for fidelity.
Assert the contract
Check status codes, headers, and body shape - the things a client depends on. e2e tests are where you catch a guard that returns 500 instead of 401, or a validation error whose message changed.
Tear down cleanly
Always await app.close() in afterAll. Leaked apps hold open ports and DB handles, turning a green suite into flaky failures on CI.
Example
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from '../src/app.module';
import { PaymentGateway } from '../src/payments/payment.gateway';
describe('Orders (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
// swap only the external boundary
.overrideProvider(PaymentGateway)
.useValue({ charge: async () => 'fake-charge-id' })
.compile();
app = moduleFixture.createNestApplication();
// mirror main.ts so the test hits the real pipeline
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
});
afterAll(async () => {
await app.close(); // release ports & handles
});
it('rejects an invalid body with 400', () => {
return request(app.getHttpServer())
.post('/orders')
.send({ items: [] }) // fails @IsNotEmpty
.expect(400);
});
it('creates an order with 201', () => {
return request(app.getHttpServer())
.post('/orders')
.send({ items: ['sku-1'] })
.expect(201)
.expect((res) => {
expect(res.body.data).toHaveProperty('orderId');
});
});
});When to use it
- A team writes e2e tests that boot the real NestJS app and use supertest to verify the full POST /users → database → response flow including guards and pipes.
- A developer overrides the database provider in e2e tests to use an in-memory SQLite connection so tests do not need a live database server.
- An engineer seeds test data in beforeAll and cleans it in afterAll using the real TypeORM repository inside the testing module.
More examples
E2E test setup boilerplate
Bootstraps the real app with the ValidationPipe and uses supertest to make an HTTP request, asserting the response status and body shape.
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('UsersController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
});
afterAll(() => app.close());
it('POST /users creates a user', () => {
return request(app.getHttpServer())
.post('/users')
.send({ name: 'Alice', email: '[email protected]' })
.expect(201)
.expect(res => expect(res.body).toHaveProperty('id'));
});
});Override provider in e2e test
Uses overrideProvider to replace MailService with a mock so e2e tests do not send real emails while still exercising the full app code.
const module = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(MailService)
.useValue({ sendWelcome: jest.fn() }) // no real emails in e2e tests
.compile();
const app = module.createNestApplication();
await app.init();
// MailService.sendWelcome won't send real emails during the testTest JWT-protected route
Verifies the full auth flow end-to-end: unauthenticated request returns 401, login provides a token, and the token enables access to /profile.
it('GET /profile requires auth', async () => {
// No token — expect 401
await request(app.getHttpServer()).get('/profile').expect(401);
// Login first to get a token
const { body } = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: '[email protected]', password: 'pass123' });
// Authenticated request
await request(app.getHttpServer())
.get('/profile')
.set('Authorization', `Bearer ${body.access_token}`)
.expect(200);
});
Discussion