Extending framework mocks
Adding a mock for another module
How to give a module a test double, whether it is one of ours or one of yours.
Mocking your own module
An application module needs no support from this entry point. Ship a test double next to the module and apply it alongside the framework mocks.
Two seams matter, and the second is the one teams miss:
- A client seam β
setClient, the wayMsalConfigurator.setClientandsetServiceDiscoveryClientdo β so the object performing I/O can be swapped. - Configuration on the builder β so a test can adjust behaviour without constructing a client at all.
ServiceDiscoveryMockConfigurator.addServiceis the reference: the configurator accumulates config, and the client is built from it when the module assembles its config.
// @my-app/module-invoices/mock
export const mockInvoices = (configurator, options = {}) => {
configurator.addConfig(
configureInvoices((builder) => {
builder.setClient({
getInvoice: async (id) => ({ id, total: options.total ?? 100 }),
});
}),
);
};// @my-app/module-invoices β the module's own type, exported for tests
export type InvoiceModule = Module<'invoices', InvoiceClient, InvoiceConfigurator>;Pass that descriptor to mockFramework as a type argument, and the module is typed on both the configurator and the returned instance:
const fusion = await mockFramework<[InvoiceModule]>((configurator) => {
enableInvoicesMock(configurator, { total: 42 });
});
await fusion.modules.invoices.getInvoice('inv-1'); // typed, no castGiving your module the same accessor as .msal and .serviceDiscovery
enableInvoicesMock(configurator, options) is enough on its own β the configurator it builds is discarded once configuration runs, which is fine when a test only ever sets options up front. Reach for an accessor when a test needs the configurator itself, for example to assert against it after the fact.
Subclass FrameworkMockConfigurator and use the protected _pin/_getConfig pair it exposes for exactly this β the same mechanism .msal and .serviceDiscovery are built from:
class AppMockConfigurator extends FrameworkMockConfigurator<[InvoiceModule]> {
constructor() {
super();
this._pin(invoiceMockModule);
}
get invoices(): InvoiceMockConfigurator {
return this._getConfig('invoices');
}
}_pin replaces the module's own configure factory with one that always returns the same instance β pinning it before initialization runs is what lets a test reach .invoices synchronously and have it be the configurator the module is actually built from. _getConfig looks that instance up by name, throwing if nothing was pinned for it.
What is not covered yet
.services and .telemetry are already reachable on FrameworkMockConfigurator β their configure factories take no ref, so they were safe to pin the same way .msal and .serviceDiscovery are. What is missing is a test double behind them: neither module has a src/mock/ folder yet, so anything issuing an actual request through them still reaches the network. Adding one means creating that folder in that module, then pinning its mock configurator with _pin and exposing it with _getConfig on FrameworkMockConfigurator, replacing the real module descriptor pinned there today.
event is not pinned at all, deliberately: its configure factory reads ref to wire event bubbling to a parent event provider when FrameworkMockConfigurator is hoisted inside a host framework. Pinning would call configure() with ref always undefined, silently breaking that bubbling β so it is left to build the normal way, from the module system's own configure phase, where ref is actually known.
.http
.http is the real HttpClientConfigurator built from @equinor/fusion-framework-module-http β there is no separate mock client. Register a short-circuiting middleware through addMiddleware to answer requests without touching the network:
configurator.http.configureClient('catalog', { baseUri: 'https://api.example.com' });
configurator.http.addMiddleware(async (uri, init, next) =>
uri === 'https://api.example.com/items' ? Response.json([{ id: 1 }]) : next(uri, init),
);
const items = await fusion.modules.http.createClient('catalog').json('/items');A middleware is (uri, init, next) => Response | Promise<Response> | Observable<Response> β return a Response to answer the request, or call and return next(uri, init) to fall through to whichever middleware (or the real network call) is registered next. Two ways to fill that seam:
addMiddlewareβ a hand-rolled middleware for a handful of routes, as above.createOpenApiMockMiddleware(@equinor/fusion-framework-module-http/mock) β adapts an@equinor/fusion-openapi-mockinstance (createOpenApiMock(document)), so a realopenapi.json/openapi.yamlfakes every response with no handlers written at all until an edge case needs overriding.
.context
.context is backed by ContextMockConfigurator (@equinor/fusion-framework-module-context/mock): context items live in an in-memory pool instead of a real context API, so a test needs no HTTP mock and no service-discovery mock to seed a known item.
configurator.context.setCurrentContext({ id: 'my-ctx', type: { id: 'ProjectMaster' }, value: {} });
fusion.modules.context.currentContext; // the seeded item, resolved on startupReal ContextProvider behaviour β validateContext, resolveContext, parent-context propagation β still runs against the seeded data; only the data source is substituted. Two layers cover different needs:
setCurrentContext/setContexts/addContext/setRelatedContextsβ a friendly, context-domain vocabulary for the common case: seed a known item, get it back.setResolverβ an escape hatch for a customresolveContextstrategy or a shape the friendly layer did not anticipate.
Seeding a context item this way is one of two ways to fake context in a test β the other is mocking the context API's HTTP responses directly (with .http, optionally paired with createOpenApiMockMiddleware), which exercises the real ContextModuleConfigurator/services/HTTP pipeline instead of substituting it. Reach for .context to seed one known item with no transport involved; reach for .http when the test needs to cover that pipeline itself.