NestJS in Production · Configuration, Testing, Data and Operations

Testing with Test.createTestingModule

Write fast unit tests by overriding providers in a testing module, and end-to-end tests that call the HTTP layer.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

Tests for a CheckoutService that depends on a payment gateway: a unit test with the gateway overridden by a fake, and an end-to-end test that boots the controller in a testing module and sends real HTTP requests, both run with node:test.

The idea

  • Test.createTestingModule({...}) builds a module like the real one; .overrideProvider(X).useValue(fake) replaces dependencies.
  • Unit tests get services from the module (moduleRef.get(Service)) and call them directly.
  • End-to-end tests create a Nest application from the testing module (createNestApplication()), so pipes, guards and filters run as in production.
  • Use the same global setup (pipes, filters) in tests as in main.ts: put it in a shared configureApp(app) function.

The code

Unit and end-to-end tests with overridden providers
import "reflect-metadata";
import { test } from "node:test";
import assert from "node:assert/strict";
import { Body, Controller, Inject, Injectable, Module, Post } from "@nestjs/common";
import { Test } from "@nestjs/testing";

const GATEWAY = Symbol("GATEWAY");
interface Gateway { charge(cents: number): Promise<{ ok: boolean }>; }

@Injectable()
class CheckoutService {
  constructor(@Inject(GATEWAY) private gateway: Gateway) {}
  async checkout(cents: number) {
    if (cents <= 0) return { status: "rejected", reason: "empty cart" };
    const result = await this.gateway.charge(cents);
    return { status: result.ok ? "paid" : "declined" };
  }
}

@Controller("checkout")
class CheckoutController {
  constructor(private svc: CheckoutService) {}
  @Post() pay(@Body() body: { cents: number }) { return this.svc.checkout(body.cents); }
}

@Module({ controllers: [CheckoutController], providers: [CheckoutService, { provide: GATEWAY, useValue: { charge: async () => { throw new Error("real gateway in tests!"); } } }] })
class CheckoutModule {}

test("declined payments are reported", async () => {
  const moduleRef = await Test.createTestingModule({ imports: [CheckoutModule] })
    .overrideProvider(GATEWAY).useValue({ charge: async () => ({ ok: false }) })
    .compile();
  assert.deepEqual(await moduleRef.get(CheckoutService).checkout(500), { status: "declined" });
});

test("POST /checkout pays through the HTTP layer", async () => {
  const calls: number[] = [];
  const moduleRef = await Test.createTestingModule({ imports: [CheckoutModule] })
    .overrideProvider(GATEWAY).useValue({ charge: async (c: number) => { calls.push(c); return { ok: true }; } })
    .compile();
  const app = moduleRef.createNestApplication({ logger: false });
  await app.listen(0, "127.0.0.1");
  const r = await fetch(`http://127.0.0.1:${app.getHttpServer().address().port}/checkout`, {
    method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cents: 1999 }) });
  assert.equal(r.status, 201);
  assert.deepEqual(await r.json(), { status: "paid" });
  assert.deepEqual(calls, [1999]);
  await app.close();
});

Output

✓ declined payments are reported
✓ POST /checkout pays through the HTTP layer
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Tests that hit real external services because a provider wasn't overridden.
  • End-to-end tests without the global pipes and filters, so they pass while production rejects requests.
  • Rebuilding a large testing module for every tiny test; create it once per file when tests don't mutate it.

Key points

  • Test.createTestingModule plus overrideProvider gives fast, isolated tests.
  • Use createNestApplication() to test through HTTP with the full request pipeline.
  • Share app configuration between main.ts and tests.

Further reading: Testing

Practice questions

How do you replace a dependency in a NestJS test?

Answer

Build a testing module with Test.createTestingModule and call overrideProvider(TOKEN).useValue(fake) (or useClass/useFactory) before compile().

When should you use createNestApplication() in tests?

Answer

For end-to-end tests that exercise routing, pipes, guards, interceptors and filters through real HTTP requests.

Why share global setup between main.ts and tests?

Answer

Pipes, filters and interceptors change behaviour; if tests don't apply the same setup they test a different application.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?