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

Example · typescript
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.

Example · ts
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.

Example · ts
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 test

Test 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.

Example · ts
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

  • Be the first to comment on this lesson.