---
url: https://masyaka.github.io/injecute/guide/testing.md
description: >-
  Replace dependencies in tests with isolated forks, clean up with await using,
  and assert error codes.
---

# 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](../playground?example=testing)

`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](./request-state.md#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](./custom-registration.md#checking-the-wiring).
* To create services at startup, use
  [`preload()`](./recipes.md#checking-the-wiring-at-startup) 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](./middleware-events.md#test-doubles).

## 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' }),
  );
});
```
