Replace services in tests
Replace a registered service with a test double in an integration test at the DI boundary with app.test({ replace }) and handle.reset({ replace }).
You want one route in an integration test to hit a fake instead of the real thing: a database that returns canned rows, a mailer that records calls instead of sending. Flare swaps the service at the DI boundary, so the route, the handler, and every inject call site stay exactly as they ship. Pass a replace map to app.test() or handle.reset(), and the substituted graph still runs the validator suite before any request flows.
This is not module mocking and it is not new RealService() by hand. The double is a real registration that takes the original’s place in the container.
- Register the real service on the host as usual (
host.scoped()orhost.singleton()). - Write a double that
extendsthe real service and overrides the methods you want faked. - Build the test app and pass
replace: new Map([[Real, Double]])toapp.test(). - Send a request with
handle.fetchand assert on theResponse.
import { FlareHost, FlareResponse, FlareService } from "@flare-ts/core";import { node } from "@flare-ts/core/node";import { strict as assert } from "node:assert";
// FLARE_MODE=test must be set before this module loads (see Notes).
class Db extends FlareService { public static override deps = []; lookup(): { ok: boolean; source: string } { return { ok: true, source: "real" }; }}
const host = new FlareHost(node);host.scoped(Db);host.http.get( "/health", { inject: { db: Db } }, (_ctx, scope) => new FlareResponse(200, scope.db.lookup()),);
class StubDb extends Db { override lookup() { return { ok: true, source: "stub" }; }}
const handle = await host.build().test({ replace: new Map([[Db, StubDb]]) });
const res = await handle.fetch("GET /health");assert.equal(res.status, 200);assert.deepEqual(await res.json(), { ok: true, source: "stub" });
await handle.stop();The route’s scope.db resolves a StubDb instance because the container now answers the Db token with the replacement class. The handler code never changes.
Swap between scenarios with reset
Section titled “Swap between scenarios with reset”app.test() runs once per host instance. To change the double between cases in one file, call handle.reset({ replace }). It tears the app down, restores the original registrations, applies the new map, and restarts on the same handle.
import { FlareHost, FlareResponse, FlareService } from "@flare-ts/core";import { node } from "@flare-ts/core/node";import { afterAll, beforeAll, expect, it } from "vitest";import type { TestAppHandle } from "@flare-ts/core/testing";
class Db extends FlareService { public static override deps = []; lookup(): { ok: boolean; source: string } { return { ok: true, source: "real" }; }}
class StubDb extends Db { override lookup() { return { ok: true, source: "stub" }; }}
const host = new FlareHost(node);host.scoped(Db);host.http.get( "/health", { inject: { db: Db } }, (_ctx, scope) => new FlareResponse(200, scope.db.lookup()),);
let handle: TestAppHandle;
beforeAll(async () => { handle = await host.build().test();});
afterAll(async () => { await handle.stop();});
it("hits the real Db by default", async () => { await handle.reset(); const res = await handle.fetch("GET /health"); expect(await res.json()).toEqual({ ok: true, source: "real" });});
it("hits the stub when replaced", async () => { await handle.reset({ replace: new Map([[Db, StubDb]]) }); const res = await handle.fetch("GET /health"); expect(await res.json()).toEqual({ ok: true, source: "stub" });});replace is a Map<ServiceToken, ServiceClass>. The exported AppTestOptions type is the shape both app.test() and handle.reset() accept.
The replacement plays by the same rules
Section titled “The replacement plays by the same rules”Flare checks this before the substituted graph compiles:
- The map key must be a registered service token.
- The replacement class must extend the token (
instanceofis checked). - After substitution, the service validator re-runs against the post-replacement graph.
A double with invalid static deps fails setup with FlareTestError, not an assertion inside your test body.
Unit level, without a host
Section titled “Unit level, without a host”When you want a class under test on its own, with no app and no HTTP pipeline, use mockContainer from @flare-ts/core/testing. See Unit-test a controller and Using DI in apps.
FLARE_MODE=testmust be set before the host module is imported.- Singletons are instantiated at
app.test()/handle.reset()in test mode, not athost.build(), soreplacesubstitutes the class before any constructor runs. handle.reset()with no args restores the original registrations. Callhandle.stop()inafterAllso singletononStop()hooks run.
See also
Section titled “See also”- Testing: the full
TestAppHandlesurface - Write your first test: build a handle before swapping services
- Named inject map: inline route
injectmaps