TestingSupported
createTestApp builds an app from a module with DI overrides and drives routes in-process — no socket, no live server. It's the @nestjs/testing analogue: swap a provider for a fake, then assert on the response.
In-process requests#
import { bodyText } from '@zmdb/web';
import { createTestApp } from '@zmdb/web/testing';
const app = createTestApp(AppModule);
const res = await app.request({ method: 'GET', path: '/hello', headers: {} });
expect(JSON.parse(await bodyText(res))).toEqual({ message: 'hello' });request() returns the production WebResponse. Use bodyText() to read text, bytes or a stream uniformly; reading a stream consumes it.
Overriding a provider#
Replace any provider before controllers are built, so the controller under test injects your stub:
const stub = { greet: () => 'stubbed' };
const app = createTestApp(AppModule, {
overrides: [{ token: GreeterToken, useValue: stub }],
});
await app.request({ method: 'GET', path: '/hello', headers: {} });
// → { msg: 'stubbed' }
app.get(GreeterToken) === stub; // resolve any provider to assert on a spyThe same override applies inside a lazy module. Its route is available immediately, and the first test request constructs the deferred controller with the stub.
Lifecycle in tests#
createTestApp is an AsyncDisposable, so await using cleans up:
await using app = createTestApp(AppModule);
await app.init(); // runs onModuleInit hooks
// ... assertions ...
// dispose runs onShutdown hooks at scope exitThe test harness uses the same provider/controller construction ledger as createApp: a provider resolved with app.get() participates in lifecycle, and an unresolved factory is not constructed just for shutdown.
Design notes#
- Overrides apply before build — the injected value is the override, through the same container.
- In-process —
requestuses the router's dispatcher; no socket. - No
ason the consumer surface. - Granular import:
import { createTestApp } from '@zmdb/web/testing'.