Skip to content
NestJS ns testing 4 min read

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 means getHttpServer() 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.

AssertionChecks
.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 AppModule for 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 as APP_* providers so they load automatically.
  • Prefer returning the supertest chain (or await-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() in afterAll to release ports, connections, and pending timers.
  • Keep e2e specs in *.e2e-spec.ts under test/ so they run separately from fast unit suites.
Last updated June 14, 2026
Was this helpful?