End-to-End Testing
End-to-end (e2e) tests exercise your application the way a real client does: they boot a complete Nest application, send actual HTTP requests, and assert on the status codes and response bodies that come back. Unlike unit tests that isolate a single provider, e2e tests verify that controllers, pipes, guards, interceptors, and the DI graph all wire together correctly. NestJS ships first-class support for this through the @nestjs/testing package and supertest, so you can drive the live HTTP layer without ever opening a network port.
How e2e testing works in Nest
A Nest e2e test follows three phases. First you compile a TestingModule from your real AppModule (or a sliced subset of it). Then you call createNestApplication() and init() to produce a running INestApplication instance. Finally you pass app.getHttpServer() to supertest, which issues requests directly against the underlying HTTP listener and lets you chain assertions on the result.
Nest’s CLI scaffolds a test/ directory with a dedicated jest-e2e.json config that matches *.e2e-spec.ts files. Keeping e2e specs separate from unit specs lets you run them independently and avoids loading the full application graph during fast unit runs.
npm run test:e2e
Bootstrapping the application
The Test.createTestingModule builder accepts the same metadata as @Module. For e2e tests you typically import the whole AppModule so the real routing table is registered. After compiling, create and initialize the application.
// test/cats.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Cats (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('GET /cats returns the seeded list', () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
});
});
});
Calling
app.init()registers routes, runs lifecycle hooks (onModuleInit,onApplicationBootstrap), and connects any microservice transports. Skipping it meansgetHttpServer()returns a server with no routes mounted.
Issuing requests and asserting responses
supertest wraps the HTTP server and exposes a fluent API. Each verb method (get, post, put, patch, delete) returns a chainable request you can decorate with headers, query strings, and a JSON body. Returning the chain from a test lets Jest await it.
it('POST /cats creates a cat and echoes it back', () => {
return request(app.getHttpServer())
.post('/cats')
.send({ name: 'Mittens', age: 3 })
.set('Accept', 'application/json')
.expect(201)
.expect('Content-Type', /json/)
.then((res) => {
expect(res.body).toMatchObject({ name: 'Mittens', age: 3 });
expect(res.body.id).toBeDefined();
});
});
You can mix declarative .expect() matchers with imperative assertions in .then(). The table below summarizes the most common assertion styles.
| Assertion | Checks |
|---|---|
.expect(200) | Numeric status code |
.expect('Content-Type', /json/) | Header value against a regex |
.expect({ ok: true }) | Exact JSON body equality |
.expect((res) => { ... }) | Custom callback that throws on failure |
Output:
PASS test/cats.e2e-spec.ts
Cats (e2e)
✓ GET /cats returns the seeded list (38 ms)
✓ POST /cats creates a cat and echoes it back (21 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Applying global pipes, filters, and interceptors
A subtle but important detail: enhancers you register in main.ts via app.useGlobalPipes(...) do not run automatically in tests, because your bootstrap() function is never called. To get production-accurate behavior, apply the same global configuration on the test app instance before init().
import { ValidationPipe } from '@nestjs/common';
import { AllExceptionsFilter } from '../src/common/all-exceptions.filter';
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.useGlobalFilters(new AllExceptionsFilter());
await app.init();
});
it('rejects an invalid payload with 400', () => {
return request(app.getHttpServer())
.post('/cats')
.send({ name: '', age: 'not-a-number' })
.expect(400)
.expect((res) => {
expect(res.body.message).toEqual(
expect.arrayContaining([expect.stringContaining('age')]),
);
});
});
If your global enhancers are registered with DI tokens (using APP_PIPE, APP_FILTER, etc.) inside a module’s providers, they are part of the compiled graph and will run without any extra wiring — that is the most reliable way to keep test and runtime behavior in sync.
Overriding providers for e2e
Even in e2e tests you often want to swap out a real dependency — for example, replacing an email service or pointing a repository at an in-memory store. Use overrideProvider on the builder before compiling.
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(MailService)
.useValue({ send: jest.fn().mockResolvedValue(true) })
.compile();
Cleaning up
Always tear the application down in afterAll (or afterEach if you recreate it per test). app.close() runs shutdown hooks, closes database connections, and frees the HTTP server, preventing the dreaded “a worker process has failed to exit gracefully” warning from Jest.
afterAll(async () => {
await app.close();
});
Best practices
- Boot the full
AppModulefor true e2e coverage; slice modules only when a subset genuinely isolates the flow under test. - Re-apply global pipes, filters, and interceptors on the test
app, or register them asAPP_*providers so they load automatically. - Prefer returning the
supertestchain (orawait-ing it) so Jest tracks the async assertion correctly. - Reset or seed state between tests to keep specs independent and deterministic.
- Always
await app.close()inafterAllto release ports, connections, and pending timers. - Keep e2e specs in
*.e2e-spec.tsundertest/so they run separately from fast unit suites.