npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@setu-ts/testing

v0.11.0

Published

Test app factory, mock plugins, and request injection for Setu-TS

Readme

@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

plugins must include a runtime provider. This package depends only on common and kernel, so it cannot supply RuntimePlugin for you, and the kernel requires the runtime capability at start(). await createTestApp() with no plugins rejects with No plugin provides the mandatory 'runtime' capability.

Global middleware needs autoStart: false. start() compiles the pipeline, after which app.middleware.add(...) throws. Routes are unaffected — app.router.get(...) works on a started app.

Unhandled errors answer in the kernel's fallback format. createTestApp does not install errorHandler (this package depends only on common and kernel, not exceptions), so an unhandled error returns the kernel's { error, detail? } fallback body, not RFC 9457.

errorHandler(...) from @setu-ts/exceptions is a middleware, not a plugin — it cannot be passed to plugins (which accepts IPlugin[] and would throw TypeError: plugin.register is not a function at start()). 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 whatever errorHandler that root registered, so its error bodies are the ones production serves. This package deliberately installs no responder of its own: it 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.

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's connect() already happened. Use without for 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 with Capability '<token>' is already registered; declare provides, 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.

overrideCapability replaces, createMockPlugin provides. createMockPlugin declares the token in provides — which is what satisfies a dependent plugin's dependencies check, 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 Request body is a one-shot stream. Injecting one consumes it, so the same Request cannot be injected twice or injected and then handed to app.fetch() — the second call throws and names the cause rather than quietly sending no body. Build a separate Request per 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

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.