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.
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 sharedconfigureApp(app)function.
The code
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.createTestingModuleplusoverrideProvidergives fast, isolated tests.- Use
createNestApplication()to test through HTTP with the full request pipeline. - Share app configuration between
main.tsand 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.