NestJS — Enterprise Node.js

End-to-End (E2E) Testing

16 min Lesson 44 of 48

End-to-End (E2E) Testing

End-to-end (E2E) testing boots your entire NestJS application inside a test environment and fires real HTTP requests through it, verifying that every layer — routing, guards, pipes, services, and the database — works together correctly. Unlike unit tests that isolate a single class, E2E tests give you confidence that the wired-up application behaves correctly from the outside world's perspective.

Tools: Supertest + Jest

NestJS's scaffolding ships a test/ directory with a ready-made E2E setup. The two key pieces are:

  • Supertest — a fluent HTTP assertion library. It accepts an Express (or Fastify) HTTP server and lets you make requests and assert on status codes, headers, and bodies without ever binding to a real port.
  • Jest — the test runner. NestJS includes a separate jest-e2e config in package.json that points rootDir at test/ and sets testRegex to .e2e-spec.ts.
Run E2E tests with npm run test:e2e. This invokes jest --config ./test/jest-e2e.json, which is separate from your unit-test run (npm test). Keep the two suites separate so fast unit tests are not slowed down by database setup.

Booting the full application in a test module

Use Test.createTestingModule() — the same API as unit tests — but import your real AppModule instead of a minimal slice. Calling .compile() then app.init() starts the NestJS lifecycle (module init, onApplicationBootstrap hooks, etc.):

import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; describe('Auth (e2e)', () => { let app: INestApplication; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ whitelist: true })); await app.init(); }); afterAll(async () => { await app.close(); }); });
Mirror your main.ts setup. If main.ts registers a ValidationPipe, CORS, or a global prefix, apply the same configuration inside beforeAll. Forgetting this causes tests to pass locally but fail against the real app.

Test database setup and teardown

Running E2E tests against your production or development database is dangerous. The standard approach is to point the test run at a dedicated test database via environment variables, then clean up after each test or suite:

  • Set NODE_ENV=test and create a .env.test (or use a separate TypeORM config) that targets a test-only database or an in-memory SQLite database.
  • Run migrations before the suite starts (or use synchronize: true only for tests).
  • Truncate or roll back data between tests to avoid order-dependent failures.
// In beforeAll — run migrations so schema is fresh import { DataSource } from 'typeorm'; beforeAll(async () => { // ... create and init the app as shown above ... const dataSource = app.get(DataSource); await dataSource.runMigrations(); }); afterAll(async () => { const dataSource = app.get(DataSource); await dataSource.dropDatabase(); // wipe the test DB await dataSource.destroy(); await app.close(); });

Testing auth-protected endpoints end to end

A complete E2E auth flow typically has two phases: obtain a token, then use it. Supertest chains make this readable:

describe('POST /auth/login', () => { it('returns a JWT for valid credentials', async () => { const res = await request(app.getHttpServer()) .post('/auth/login') .send({ email: 'alice@example.com', password: 'secret' }) .expect(201); expect(res.body.access_token).toBeDefined(); }); }); describe('GET /users/me (protected)', () => { let token: string; beforeAll(async () => { const res = await request(app.getHttpServer()) .post('/auth/login') .send({ email: 'alice@example.com', password: 'secret' }); token = res.body.access_token; }); it('returns the authenticated user', () => { return request(app.getHttpServer()) .get('/users/me') .set('Authorization', `Bearer ${token}`) .expect(200) .expect((res) => { expect(res.body.email).toBe('alice@example.com'); }); }); it('rejects unauthenticated requests with 401', () => { return request(app.getHttpServer()) .get('/users/me') .expect(401); }); });

What to assert

  • Status codes — the most basic check; .expect(200), .expect(401), .expect(422).
  • Response shape — assert that required fields exist and have the expected types.
  • Side effects — after a POST that creates a resource, query the database directly (via the injected repository) to confirm the row was persisted.
  • Guard enforcement — always include a negative test: hitting a protected route without a token must return 401.
Do not rely on test order. Each test (or at minimum each describe block) should seed its own prerequisite data and clean it up afterward. Tests that depend on prior tests running first are fragile and hard to debug.

Summary

E2E tests in NestJS use Test.createTestingModule() with the real AppModule and Supertest to fire HTTP requests against the running application. Point tests at a dedicated test database, run migrations in beforeAll, and tear down in afterAll. For auth-protected routes, obtain a JWT in a setup step then attach it via the Authorization header. Always include negative tests (no token → 401) and verify database side-effects for mutation endpoints.