Documents injecute 1.x · Migrating from 0.x
Skip to content

Testing ​

Replace a dependency everywhere: isolated forks ​

An isolated fork builds every service it resolves itself. Replace the dependency in the fork and everything resolved through it uses the replacement; the app container is untouched.

ts
const app = createApp();

// Everything the test resolves is built inside the fork, with the fake database.
await using testContainer = app
  .fork({ isolated: true })
  .addInstance('db', new Database('memory://'), { replace: true });

testContainer.get('users').db.url; // "memory://"
app.get('users').db.url; // "postgres://prod": the app is untouched

Open in the playground

await using disposes whatever the test created, even when an assertion throws.

Forks made from the isolated fork, such as a scope per request, get the replacement too. To replace a service that a scope registers, see Testing scoped services.

Check the wiring ​

One test can check that every service is wired: checkWiring() reports every dependency key that nothing registers, and with resolve: true every service whose factory throws, all at once, instead of the first request finding one of them:

ts
// Checks that everything the services need is registered, then resolves every service once, in an
// isolated fork with fakes for what reaches the outside world. Every missing registration and every
// factory that throws is reported at once, instead of the first request finding one of them.
await using wiring = createApp()
  .fork({ isolated: true })
  .addInstance('db', new Database('memory://'), { replace: true });
await checkWiring(wiring, { resolve: true });
  • Without resolve, nothing runs: it only reads the registrations, so it needs no fakes. With resolve: true it creates every service once: use an isolated fork with fakes for services that reach the outside world (databases, queues, HTTP clients).
  • A failing service is reported once, with the services that need it, and its factory runs once: services that depend on it are skipped, not reported again.
  • It rejects with a WiringError (INJECUTE_WIRING_FAILED) whose message lists every problem, so a failing test shows them all; error.problems has them as data.
  • Services the compiler can't see, such as ones found in files or registered by decorators, can be declared without being registered: pass their keys, see Checking the wiring.
  • To create services at startup, use preload() instead: it stops at the first failure, which is what an app that is starting needs.

Override one cached value: setCacheInstance ​

ts
// Or override one cached value on the container itself, until reset():
const other = createApp();
setCacheInstance(other, 'db', new Database('memory://'));
other.get('users').db.url; // "memory://"
other.reset();

This changes the container you pass until the next reset(), so prefer isolated forks when tests share a container.

Temporary doubles with a middleware ​

A middleware can answer for some keys and be removed with unuse(). See Middlewares.

Asserting errors ​

Branch on the stable code, not on the message:

ts
import { InjecuteError } from 'injecute';
import { expect, it } from 'vitest';

it('requires a database', () => {
  const app = createApp();
  expect(() => app.get('users')).toThrow(
    expect.objectContaining({ code: 'INJECUTE_NOT_REGISTERED' }),
  );
});

Released under the MIT License.