zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Server framework

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 spy

The 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 exit

The 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#