@setu-ts/testing
First-party testing utilities for the Setu-TS framework.
Installation
npm install @setu-ts/testing
# Deno: deno add npm:@setu-ts/testing
Usage
createTestApp
Creates a started test application that can be exercised via inject() and fetch() without
binding a socket.
import { createMockPlugin, createTestApp } from '@setu-ts/testing';
import { RuntimePlugin } from '@setu-ts/runtime';
const app = await createTestApp({
plugins: [RuntimePlugin(), createMockPlugin({ name: 'database', service: { query: () => [] } })],
});
app.router.get('/users', (ctx) => ctx.response.json([{ id: 1 }]));
const res = await app.inject({ method: 'GET', url: '/users' });
console.log(res.statusCode); // 200
pluginsmust include a runtime provider. This package depends only oncommonandkernel, so it cannot supplyRuntimePluginfor you, and the kernel requires theruntimecapability atstart().await createTestApp()with no plugins rejects withNo plugin provides the mandatory 'runtime' capability.Global middleware needs
autoStart: false.start()compiles the pipeline, after whichapp.middleware.add(...)throws. Routes are unaffected —app.router.get(...)works on a started app.Unhandled errors answer in the kernel’s fallback format.
createTestAppdoes not installerrorHandler(this package depends only oncommonandkernel, notexceptions), so an unhandled error returns the kernel’s{ error, detail? }fallback body, not RFC 9457.
errorHandler(...)from@setu-ts/exceptionsis a middleware, not a plugin — it cannot be passed toplugins(which acceptsIPlugin[]and would throwTypeError: plugin.register is not a functionatstart()). Register it on an un-started app, then start — see the example below the callout.
Registering errorHandler on an un-started application:
import { createTestApp } from '@setu-ts/testing';
import { RuntimePlugin } from '@setu-ts/runtime';
import { errorHandler } from '@setu-ts/exceptions';
const app = await createTestApp({
plugins: [RuntimePlugin()],
autoStart: false,
});
app.middleware.add(errorHandler({ format: 'rfc9457' }), {
priority: 0,
name: 'error-handler',
});
await app.start();
Once registered, the responder seam governs the kernel’s own 404/500 terminals too.
The
app:arm dissolves this. An application built from the project’s own composition root already carries whatevererrorHandlerthat root registered, so its error bodies are the ones production serves. This package deliberately installs no responder of its own: it depends oncommonandkernelonly, so a default would mean a second RFC 9457 formatter that@setu-ts/exceptions’ own tests do not drive.
Building from the composition root
createTestApp has a second arm. Pass app instead of plugins and the test starts from the
application the project actually ships — the createApp() a scaffolded project exports from
setu.config.ts, or a starter factory’s return value — then subtracts from it:
New scaffolds return IKernelApplication (or Promise<IKernelApplication>). In an older project,
change the factory’s IApplication annotation to IKernelApplication, imported from
@setu-ts/kernel, before using the app arm. Await an async starter factory.
import { createTestApp, overrideCapability } from '@setu-ts/testing';
import { CAPABILITIES } from '@setu-ts/common';
import { createApp } from '../setu.config.ts';
const app = await createTestApp({
app: createApp(),
without: ['database'],
overrides: [overrideCapability(CAPABILITIES.MAIL, fakeMailer)],
});
Everything the root registered is present — its middleware, its error handling, its health
indicators, its route ordering — except what without explicitly removes: an excluded plugin
contributes none of those, which is the point of excluding it. A test therefore observes the
composition production has rather than a second one assembled by hand — which is what the plugins
arm inevitably drifts into.
The two arms are mutually exclusive: TestAppOptions is a union, so supplying both plugins and
app is a compile error rather than a runtime throw.
without and overrides do different things, and the difference matters.
Both are applied before createTestApp calls start(); what differs is when they take effect
during that startup.
without | overrides | |
|---|---|---|
| Applied | Before start() | Before start() |
| Takes effect | The plugin is gone from the list | Late in the same startup, after the real plugin registers |
| Effect | The plugin’s register() never runs | The plugin’s service is replaced |
Eager side effects (connect(), a broker dial) | Prevented | Already happened |
DatabasePlugin.register() calls adapter.connect(), so an override leaves a real connection
attempt in place. Use without when you need the plugin not to run at all, and overrides when you
want it composed but serving a double.
without throws naming an entry the application does not hold. A silently ignored
without: ['databse'] would run the whole test against the real plugin while reporting success.
overrideCapability
Creates a plugin that replaces an already-provided capability, leaving the rest of the composition intact.
import { overrideCapability } from '@setu-ts/testing';
import { CAPABILITIES } from '@setu-ts/common';
const app = createApp();
app.register(overrideCapability(CAPABILITIES.MAIL, { send: () => Promise.resolve() }));
await app.start();
It applies the three constraints that make a replacement plugin work: it declares no provides (a
second declaration of a live token is refused before any plugin runs), it registers with
{ override: true }, and it orders itself through an optionalDependencies edge on the token plus
PLUGIN_PRIORITY.HIGHEST — after the provider, whatever its band, and before an ordinary consumer.
It throws during start() when nothing provides the token. Without that check a mistyped token
would register the double under a nonsense name, leave the real service serving, and let the test
pass against the real dependency.
It is ordered after the provider and before ordinary consumers, so it reaches a consumer that
resolves the capability during its own register() as well as one that resolves it per request —
NotificationPlugin resolves CAPABILITIES.MAIL while registering, and an override beneath it is
seen by every notification it sends. Ordering comes from an optionalDependencies edge on the token
plus an early priority, so it works against a provider in any priority band.
Two bounds remain, both documented rather than surprising:
- It does not undo the provider’s eager side effects. The provider’s
register()has run, so a database adapter’sconnect()already happened. Usewithoutfor that. - It requires the provider to declare the token in
provides— that is what the ordering edge hangs on, and how a plugin is depended upon at all. A provider that registers a capability without declaring it fails startup withCapability '<token>' is already registered; declareprovides, or use the removal form below.
A service provided by the DI container cannot be replaced this way. With DiPlugin registered,
DecoratorPlugin puts each @Injectable class into the container rather than the kernel registry,
so overrideCapability('pricing-service', fake) throws that nothing provides the token. Use
overrideProvider for it.
To remove the provider instead of replacing it — which also prevents its eager side effects — supply the double as a provider ahead of its consumers:
await createTestApp({
app: createApp(),
without: ['mail-plugin'],
overrides: [
createMockPlugin({
name: 'mail-plugin',
provides: CAPABILITIES.MAIL,
service: fakeMailer,
priority: PLUGIN_PRIORITY.HIGH, // ahead of the consumer that captures it
}),
],
});
It also refuses a multi-provider capability. Both refusals are raised from an onInit hook
rather than from the plugin’s own register(), because the override registers early — at that point
a multi-provider capability has not accumulated its providers yet, and a token it does not shadow
may still be registered by a later plugin. Either way the application refuses to start. getAll
returns the single and multi registrations concatenated, so an override would ADD a provider while
every real one kept running and the test would report success. Detection is generic — a
post-registration provider count, not a list of known tokens — so the kernel’s five
(health-indicator, metric-registration, openapi-schema, decorator-handler, cli-command)
and any capability an application registers with { multi: true } are refused alike. Exclude the
plugin that registers the provider instead.
overrideProvider
Replaces a provider in the DI container. With DiPlugin registered, DecoratorPlugin puts each
@Injectable class into the container rather than the service registry, so overrideCapability
cannot reach it. overrideProvider registers the double on the container before DecoratorPlugin
registers the real class, and every class that injects the token, a controller included, is
constructed with the double:
import { createTestApp, overrideProvider } from '@setu-ts/testing';
const app = await createTestApp({
app: createApp(),
overrides: [overrideProvider('pricing-service', { useValue: { price: () => 0 } })],
});
The token is the class’s @Injectable({ token }) value, or the one the decorator derived from its
name. The provider is any container provider: { useValue }, { useFactory } or { useClass }. It
depends on CAPABILITIES.DI_CONTAINER, so it runs after DiPlugin and before DecoratorPlugin.
start() fails when the application has no container, when the real provider was registered before
the override, or when nothing in the application registers or injects the token, so a mistyped token
fails the test instead of passing against the real class.
overrideCapabilityreplaces,createMockPluginprovides.createMockPlugindeclares the token inprovides— which is what satisfies a dependent plugin’sdependenciescheck, and which the kernel refuses when a real plugin already declares it (Capability 'database' is provided by both 'database' and 'database-mock'). Use it in an application that does not register the real plugin.
inject
Free-function HTTP request injector with string, InjectRequest, and web-standard Request
shorthand.
import { inject } from '@setu-ts/testing';
// String shorthand (GET only)
const res = await inject(app, '/users');
// POST with JSON body
const res2 = await inject(app, {
method: 'POST',
url: '/users',
body: { name: 'test' },
});
// Web Request
const req = new Request('http://localhost/users', {
method: 'POST',
body: JSON.stringify({ name: 'test' }),
});
const res3 = await inject(app, req);
A
Requestbody is a one-shot stream. Injecting one consumes it, so the sameRequestcannot be injected twice or injected and then handed toapp.fetch()— the second call throws and names the cause rather than quietly sending no body. Build a separateRequestper call.
For an oversized multipart upload in an in-process test on Node 24 or Node 26 (up to 26.12.0), use
inject() with an encoded body (or build the multipart body from a Blob) instead of passing
file-bearing FormData to app.fetch(new Request(...)). The undici those releases bundle
(7.20.0–8.11.2) throws an uncaught ERR_INVALID_STATE when the runtime cancels that stream at
maxBodyBytes; undici 8.12.0 fixes it. Node 22, Bun, Deno, real-socket uploads and inject() do
not take that path.
createMockPlugin
Creates an IPlugin that registers a mock service under a capability token.
import { createMockPlugin } from '@setu-ts/testing';
const mockDb = createMockPlugin({
name: 'database',
service: { query: () => [], connect: () => {} },
});
createTestContext
Builds a contract-faithful IRequestContext for unit-testing middleware and handlers in isolation.
import { createTestContext } from '@setu-ts/testing';
const ctx = createTestContext();
expect(ctx.id).toBe('test-ctx');
expect(ctx.startTime).toBe(0); // monotonic, never Date.now()
FixtureManager
Collects mocks and plugins, produces IPlugin[], resets between tests.
import { createTestApp, FixtureManager } from '@setu-ts/testing';
import { RuntimePlugin } from '@setu-ts/runtime';
const fixtures = new FixtureManager();
beforeEach(async () => {
fixtures.mock('database', { query: () => [] }).mock('cache', { get: () => null });
const app = await createTestApp({
plugins: [RuntimePlugin(), ...fixtures.plugins()],
});
});
afterEach(() => fixtures.reset());
Configuration Options
createTestApp(options?: TestAppOptions):
A union of two mutually exclusive arms.
TestAppFromPlugins — assemble by hand:
| Option | Type | Default | Behavior |
|---|---|---|---|
plugins | IPlugin[] | [] | Pre-registered before start(). Must include a runtime capability provider. |
autoStart | boolean | true | false returns the un-started app, needed to add plugins or global middleware. |
TestAppFromApp — start from the composition root:
| Option | Type | Default | Behavior |
|---|---|---|---|
app | IKernelApplication | required | An already-constructed, not-yet-started application. |
without | readonly string[] | [] | Plugin names dropped before start(), so their register() never runs. Throws on a name the app does not hold. |
overrides | readonly IPlugin[] | [] | Plugins appended after without — usually overrideCapability results. |
autoStart | boolean | true | As above. |
overrideCapability(token: CapabilityToken, service: object) takes no options; it throws at
register() when nothing provides token.
createMockPlugin(options: MockPluginOptions):
| Option | Type | Default | Behavior |
|---|---|---|---|
name | string | required | Plugin name, and the token when provides is omitted. |
service | object | required | The mock service registered under the token. |
provides | string | name | Capability token override. |
priority | number | omitted | Registration priority. |
register | (ctx: IPluginContext) => void | Promise<void> | omitted | Extra registration after the service. |
createTestContext(options?: TestContextOptions) accepts request, body, runtime, startTime,
services, response, params, query, state and signal. startTime takes precedence over
runtime.hrtime() and must be a monotonic reading — never Date.now(). signal precedence is
request.signal > signal > a live never-aborting signal, matching the kernel.
See Testing Package in PUBLIC_API.md for the full option tables and notes.
API Reference
createTestApp— Test application factorycreateMockPlugin— Mock plugin builderoverrideCapability— Capability replacement plugin builderoverrideProvider— DI container provider replacement plugin builderinject— Free-function request injectorcreateTestContext— Mock request context builderMockServiceRegistry/MockResponse— Kernel-faithful doublesFixtureManager— Multi-mock fixture managercollectStream— Streaming response reader
Exports
| Export | Kind |
|---|---|
collectStream | function |
createMockPlugin | function |
createTestApp | function |
createTestContext | function |
inject | function |
overrideCapability | function |
overrideProvider | function |
FixtureManager | class |
MockResponse | class |
MockServiceRegistry | class |
IKernelApplication | interface |
InjectRequest | interface |
InjectResponse | interface |
MockPluginOptions | interface |
StreamingBody | interface |
TestAppFromApp | interface |
TestAppFromPlugins | interface |
TestContextOptions | interface |
TestAppOptions | type |
Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it
drifts.
API reference
Testing Package (@setu-ts/testing)
First-party testing utilities for the Setu-TS framework: a test application factory, mock plugin builder, request injector, mock request context, service registry double, fixture manager, and streaming-response reader.
Exports
| Export | File | Description |
|---|---|---|
createTestApp | src/test-app.ts | Test application factory |
TestAppOptions | src/test-app.ts | Factory options (union of the two arms below) |
TestAppFromPlugins | src/test-app.ts | Hand-assembled arm |
TestAppFromApp | src/test-app.ts | Composition-root arm |
overrideCapability | src/override-capability.ts | Capability replacement plugin builder |
overrideProvider | src/override-provider.ts | DI container provider replacement builder |
createMockPlugin | src/mock-plugin.ts | Mock plugin builder |
MockPluginOptions | src/mock-plugin.ts | Builder options |
collectStream | src/inject.ts | Collect streaming response body |
inject | src/inject.ts | Inject HTTP requests into test app |
StreamingBody | src/inject.ts | Stream collector result shape |
createTestContext | src/mock-context.ts | Create a mock IRequestContext |
TestContextOptions | src/mock-context.ts | Context builder options |
MockResponse | src/mock-context.ts | Fake IResponse double |
MockServiceRegistry | src/mock-registry.ts | Fake IServiceRegistry double |
FixtureManager | src/fixtures/fixture-manager.ts | Assemble mock plugins per-test |
IKernelApplication | (re-export from @setu-ts/kernel) | Kernel application interface |
InjectRequest | (re-export from @setu-ts/kernel) | Shape for inject() request |
InjectResponse | (re-export from @setu-ts/kernel) | Shape for inject() response |
Registration
import { createTestApp } from '@setu-ts/testing';
import { RuntimePlugin } from '@setu-ts/runtime';
import { DatabasePlugin } from '@setu-ts/database-plugin';
const app = await createTestApp({
plugins: [RuntimePlugin(), DatabasePlugin({ type: 'memory' })],
});
Or from the application the project already composes, which keeps the test’s composition — middleware, error handling, health indicators, route ordering — the one production has:
import { createTestApp, overrideCapability } from '@setu-ts/testing';
import { CAPABILITIES } from '@setu-ts/common';
import { createApp } from '../setu.config.ts';
const app = await createTestApp({
app: createApp(),
without: ['database'],
overrides: [overrideCapability(CAPABILITIES.MAIL, fakeMailer)],
});
Options
TestAppOptions — createTestApp(options?) — is a union of two mutually exclusive arms, so
supplying both plugins and app is a compile error rather than a runtime throw.
New scaffold factories return IKernelApplication (or Promise<IKernelApplication>). For an older
project, replace the factory’s IApplication annotation with IKernelApplication, imported from
@setu-ts/kernel. Await an async factory before passing its result to createTestApp.
TestAppFromPlugins — assemble by hand (unit scope):
| Option | Type | Default | Behavior |
|---|---|---|---|
plugins | IPlugin[] | [] | Pre-registered before start(). Must include a runtime capability provider (RuntimePlugin(), or a mock providing CAPABILITIES.RUNTIME) whenever autoStart is left true — the kernel requires one and throws otherwise. |
autoStart | boolean | true | true calls await app.start() (no port, so no socket) before returning. false returns the un-started app — required to register further plugins or to add global middleware. |
TestAppFromApp — start from the composition root:
| Option | Type | Default | Behavior |
|---|---|---|---|
app | IKernelApplication | required | An already-constructed, not yet started application — typically a scaffolded project’s createApp() from setu.config.ts, or a starter factory’s return value. |
without | readonly string[] | [] | Plugin names dropped via IKernelApplication.unregister before start(), so each plugin’s register() — and any eager side effect in it — never runs. Throws naming an entry the app does not hold. |
overrides | readonly IPlugin[] | [] | Plugins appended after without is applied, usually overrideCapability results. Exists because the default autoStart: true leaves no window in which a caller could register them. |
autoStart | boolean | true | As above. |
without and overrides are not interchangeable. An override replaces a service after the
real plugin’s register() has run, so an eager side effect inside it — DatabasePlugin calls
adapter.connect() there — has already happened. Only without prevents that.
overrideCapability(token: CapabilityToken, service: object): IPlugin takes no options. The plugin
it returns declares no provides (a second declaration of a live token is refused by the kernel
before any plugin runs), registers with { override: true }, and orders itself through an
optionalDependencies edge on the token plus PLUGIN_PRIORITY.HIGHEST — after the provider,
whatever its band, and before an ordinary consumer. It throws during start() when nothing
provides token — a mistyped token would otherwise register the double under a nonsense name and
leave the real service serving — and when token is a multi-provider capability. Both checks
are raised from an onInit hook rather than from the plugin’s own register(), because the
override registers early: at that point a multi-provider capability has not accumulated its
providers yet, and a token the override does not shadow may still be registered by a later plugin.
onInit runs once every plugin has registered, and a throw there fails start() exactly as one in
register() would. ServiceRegistry.getAll returns the single and multi registrations
concatenated, so an override of one ADDS a provider rather than replacing the existing ones: every
real provider would still run while the caller was told the capability was overridden. Detection is
generic — a provider count taken after the write, not a list of known tokens — so the kernel’s five
(health-indicator, metric-registration, openapi-schema, decorator-handler, cli-command)
and any capability an application registers itself with { multi: true } are refused alike. The
count resolves nothing that is being replaced: the override lands in the single map first, so a
registerFactory provider is never constructed. Exclude the plugin that registers the provider
instead.
overrideCapability is ordered after the provider and before ordinary consumers, so it reaches
a consumer that resolves the capability during its own register() as well as one that resolves it
per request — NotificationPlugin resolves CAPABILITIES.MAIL while registering, and an override
beneath it is seen by every notification it sends. Ordering comes from an optionalDependencies
edge on the token, which the resolver visits ahead of any priority number, plus an early priority
that places it before a PLUGIN_PRIORITY.NORMAL consumer — so a provider in any band is replaced.
Two bounds remain. It does not undo the provider’s eager side effects: the provider’s
register() has run, so a database adapter’s connect() already happened — without is what
prevents that. And it requires the provider to declare the token in provides, which is what
the ordering edge hangs on and how a plugin is depended upon at all; a provider that registers a
capability without declaring it fails startup with Capability '<token>' is already registered. For
either, exclude the provider and supply the double ahead of its consumers —
without: ['mail-plugin'] plus a createMockPlugin at PLUGIN_PRIORITY.HIGH.
overrideProvider<T>(token: ContainerToken<T>, provider: Provider<T>, options?: ProviderOptions<T>): IPlugin
replaces a provider in the DI container, which overrideCapability cannot reach: with DiPlugin
registered, DecoratorPlugin puts each @Injectable class into the container, and the container
refuses a second registration of a token. The plugin depends on CAPABILITIES.DI_CONTAINER and
registers at PLUGIN_PRIORITY.NORMAL, so it runs after DiPlugin and before DecoratorPlugin
(PLUGIN_PRIORITY.LOW). It registers the double on the container, then publishes a container that
swallows the application’s later registration of the same token and passes every other call through,
so every class that injects the token is constructed with the double. start() fails from
register() when the application has no container or the real provider was registered first, and
from an onInit hook when nothing registered, resolved or checked for the token, which is how a
mistyped token surfaces. The token may be a string, a createToken token or a class (M125); the
double also applies inside request scopes (forRequest), and dispose() passes through to the real
container.
overrideCapabilityreplaces;createMockPluginprovides.createMockPlugindeclares the token inprovides, which satisfies a dependent plugin’sdependenciescheck and which the kernel refuses when a real plugin already declares it. Use it in an application that does not register the real plugin.
Unhandled errors. Neither arm installs errorHandler — this package depends on common and
kernel only, so a default would mean a second RFC 9457 formatter that @setu-ts/exceptions’ own
tests do not drive. The plugins arm therefore answers the kernel’s { error, detail? } fallback;
the app: arm inherits whatever responder the composition root registered, and is the answer for
response-shape fidelity.
MockPluginOptions — createMockPlugin(options):
| Option | Type | Default | Behavior |
|---|---|---|---|
name | string | required | Plugin name, and the capability token when provides is omitted. |
service | object | required | The mock service registered under the token. |
provides | string | name | Overrides the token when the plugin name and capability token differ. |
priority | number | omitted | Registration priority passed to the kernel resolver. |
register | (ctx: IPluginContext) => void | Promise<void> | omitted | Extra registration (middleware, routes, hooks) run after the service is registered. |
TestContextOptions — createTestContext(options?):
| Option | Type | Default | Behavior |
|---|---|---|---|
request | Partial<IRequest> | GET http://localhost/ | Overrides on the mock request (method, url, path, headers, ip, user, tenant, signal). |
body | unknown | none | Backs json(), text() and bytes(). A non-string body is JSON-stringified once, so all three readers agree. |
runtime | IRuntimeServices | internal monotonic fake | Supplies ctx.id (uuid()) and ctx.startTime (hrtime()). |
startTime | number | runtime.hrtime() (0) | Highest-precedence monotonic origin — never pass Date.now(). |
services | IServiceRegistry | new MockServiceRegistry() | Any implementation, including a real kernel registry from a started app. |
response | IResponse | new MockResponse() | The response builder on ctx.response. |
params | Record<string, string> | {} | Path parameters. |
query | Record<string, string> | parsed from the request URL | Query parameters. |
state | Map<string, unknown> | new Map() | Request-scoped state. |
signal | AbortSignal | live, never-aborting | ctx.signal. Precedence: request.signal > signal > default, matching the kernel. |
Programmatic Testing
import { collectStream, createMockPlugin, createTestContext, inject } from '@setu-ts/testing';
import { FixtureManager, MockServiceRegistry } from '@setu-ts/testing';
// Mock a plugin service
const mockDb = createMockPlugin({
name: 'database',
service: { find: async () => [] },
});
// Use FixtureManager to assemble mocks
const fixtures = new FixtureManager();
fixtures.mock('cache', { get: async () => null, set: async () => {} });
// Inject a request directly
const response = await inject(app, {
method: 'GET',
url: '/users',
});
// Create a fake context for handler-level tests
const ctx = createTestContext();
const mockRegistry = new MockServiceRegistry();
// Register a streaming route so collectStream has data to read
app.router.get('/stream', (c) => {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode('hello'));
controller.enqueue(new TextEncoder().encode(' world'));
controller.close();
},
});
return c.response.stream(stream);
});
// Collect a streaming body — use fetch() which returns a real web Response
const fetchRes = await app.fetch(new Request('http://localhost/stream'));
const stream = await collectStream(fetchRes);
console.log(stream.text); // → "hello world"
Notes
- The testing package does not start an HTTP server;
createTestAppreturns a started kernel application that can be exercised viainject()orfetch(). pluginsmust include a runtime provider. The package depends only oncommonandkernel, so it cannot importRuntimePluginto supply one for you, and the kernel treats theruntimecapability as mandatory atstart().await createTestApp()with no plugins therefore rejects withNo plugin provides the mandatory 'runtime' capability. PassRuntimePlugin(), or for a dependency-free unit testcreateMockPlugin({ name: 'runtime', service: fakeRuntime, provides: CAPABILITIES.RUNTIME }).- Adding global middleware requires
autoStart: false.start()compiles the pipeline, after whichapp.middleware.add(...)throwsCannot add middleware after the pipeline has been compiled.Routes are unaffected —app.router.get(...)works on a started app. - A mock providing only
runtimecannot servefetch(). The realRuntimePluginalso provideshttp-adapter; without itapp.fetch()throwsNo HTTP adapter registered.inject()needs onlyruntime, so unit tests useinject()andfetch()assertions belong to integration/e2e tests on the realRuntimePlugin(). - The free-function
inject()accepts a webRequest, and reading its body consumes it. The sameRequestcannot be injected twice, nor injected and then passed toapp.fetch()— the second call throws and names the cause instead of silently sending no body. Build a separateRequestper call. inject()returnsbody: string | null, where a byte body fromresponse.send(bytes)is UTF-8 decoded andnullmeans the response genuinely had no body. A streaming response cannot be rendered as a string without draining the live stream, soinject()throws for one — read those throughapp.fetch()pluscollectStreaminstead.createTestContextuses a built-in monotonic fake runtime (hrtime: () => 0) by default — inject your ownIRuntimeServiceswhen you need controllable timing. Its timers are inert no-ops, so a fixture can never leak a callback past the test that created it.MockServiceRegistryandMockResponsereproduce their kernel counterparts’ observable semantics (factory caching, single-vs-multi registration andgetAllmerge order, override policy, the verbatim not-registered message,content-typedefaults, and thesnapshot()discriminated union) so a test cannot pass against the double and fail against the real thing.