Testing with Jest

Unit testing a service with a mocked dependency, and e2e testing a controller with supertest.

Nest and Jest

Every project scaffolded with nest new already has Jest configured and ready to run — unit test files live next to the code they test as *.spec.ts, and end-to-end test files live in a separate test/ directory as *.e2e-spec.ts. Nest's own @nestjs/testing package builds on top of Jest with one key addition: Test.createTestingModule, which mirrors the real @Module() setup covered elsewhere in this track but lets any provider be swapped out for a test double, using the exact same override mechanism the real DI container uses to resolve dependencies.

Unit testing a service with a mocked dependency

This continues the OrdersService/UsersService example from the modules-and-dependency-injection page in this track, where OrdersService depends on UsersService to look up a user when creating an order:

Typescript
// orders.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class OrdersService {
  constructor(private readonly usersService: UsersService) {}

  createOrder(userId: number, item: string) {
    const user = this.usersService.findOne(userId);
    return { user, item };
  }
}

A unit test for OrdersService shouldn't need a real UsersService (which, in a real app, might hit a database) — it only needs something satisfying the same shape:

Typescript
// orders.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { OrdersService } from './orders.service';
import { UsersService } from '../users/users.service';

describe('OrdersService', () => {
  let service: OrdersService;
  let usersService: { findOne: jest.Mock };

  beforeEach(async () => {
    usersService = { findOne: jest.fn() };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        OrdersService,
        { provide: UsersService, useValue: usersService },
      ],
    }).compile();

    service = module.get<OrdersService>(OrdersService);
  });

  it('attaches the resolved user to the created order', () => {
    usersService.findOne.mockReturnValue({ id: 1, name: 'Ada Lovelace' });

    const order = service.createOrder(1, 'Keyboard');

    expect(order.user).toEqual({ id: 1, name: 'Ada Lovelace' });
    expect(usersService.findOne).toHaveBeenCalledWith(1);
  });
});

{ provide: UsersService, useValue: usersService } is the same provider-registration syntax @Module() itself uses under the hood — it tells the testing module "whenever something asks for UsersService, hand it this plain object instead." Because OrdersService only depends on the shape of UsersService (its public methods), a hand-written fake with a jest.fn() in place of findOne is enough to isolate the test completely from any real database or network call.

e2e testing a controller with supertest

An end-to-end test takes the opposite approach: instead of faking every collaborator, it boots the real module graph and issues genuine HTTP requests against it, exercising routing, pipes, guards, and interceptors together exactly as they'd run in production.

Bash
npm install --save-dev supertest @types/supertest
Typescript
// test/users.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('UsersController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  it('/users (GET) returns the seeded user', () => {
    return request(app.getHttpServer())
      .get('/users')
      .expect(200)
      .expect((res) => {
        expect(res.body).toEqual([{ id: 1, name: 'Ada Lovelace' }]);
      });
  });

  it('/users (POST) creates a new user', () => {
    return request(app.getHttpServer())
      .post('/users')
      .send({ name: 'Grace Hopper' })
      .expect(201)
      .expect((res) => {
        expect(res.body.name).toBe('Grace Hopper');
      });
  });
});

request(app.getHttpServer()) drives supertest against the real, fully-wired Nest application — app.init() performs the same bootstrap NestFactory.create does, just without actually binding to a network port, so these requests exercise the genuine routing and middleware pipeline, not a simulation of it.

Unit tests vs e2e tests, side by side

Unit test e2e test
What's real Only the class under test The entire module graph, bootstrapped for real
Dependencies Faked with useValue/jest.fn() Real (unless explicitly overridden)
Speed Fast — no HTTP, no real I/O Slower — a real app instance, real requests
Catches Logic bugs inside one class Wiring bugs — routing, guards, pipes, module imports

Running the tests

Bash
npm run test         # unit tests: *.spec.ts
npm run test:e2e      # end-to-end tests: test/*.e2e-spec.ts
npm run test:cov      # unit tests with a coverage report

Common mistakes

  • Importing the real feature module (pulling in a real database connection or external client) into a unit test instead of overriding just the one dependency being faked — this turns a fast, isolated unit test into a slow integration test with none of the benefits of either.
  • Forgetting await app.close() in an e2e test's afterAll — this leaks open resources (like a database connection the real module opened) between test files, which can make Jest hang or print open-handle warnings.
  • Testing a controller by calling its methods directly in a unit test instead of driving it through supertest in an e2e test — calling the method directly only proves the method's own logic works, not that routing, guards, and pipes are actually wired up correctly for that route.