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.
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 untouchedawait 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:
// 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. Withresolve: trueit 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.problemshas 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
// 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:
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' }),
);
});