|
| 1 | +# Adding a mock for another module |
| 2 | + |
| 3 | +How to give a module a test double, whether it is one of ours or one of yours. |
| 4 | + |
| 5 | +## Mocking your own module |
| 6 | + |
| 7 | +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. |
| 8 | + |
| 9 | +Two seams matter, and the second is the one teams miss: |
| 10 | + |
| 11 | +1. **A client seam** — `setClient`, the way `MsalConfigurator.setClient` and `setServiceDiscoveryClient` do — so the object performing I/O can be swapped. |
| 12 | +2. **Configuration on the builder** — so a test can adjust behaviour *without* constructing a client at all. `ServiceDiscoveryMockConfigurator.addService` is the reference: the configurator accumulates config, and the client is built from it when the module assembles its config. |
| 13 | + |
| 14 | +```typescript |
| 15 | +// @my-app/module-invoices/mock |
| 16 | +export const mockInvoices = (configurator, options = {}) => { |
| 17 | + configurator.addConfig( |
| 18 | + configureInvoices((builder) => { |
| 19 | + builder.setClient({ |
| 20 | + getInvoice: async (id) => ({ id, total: options.total ?? 100 }), |
| 21 | + }); |
| 22 | + }), |
| 23 | + ); |
| 24 | +}; |
| 25 | +``` |
| 26 | + |
| 27 | +```typescript |
| 28 | +// @my-app/module-invoices — the module's own type, exported for tests |
| 29 | +export type InvoiceModule = Module<'invoices', InvoiceClient, InvoiceConfigurator>; |
| 30 | +``` |
| 31 | + |
| 32 | +Pass that descriptor to `mockFramework` as a type argument, and the module is typed on both the configurator and the returned instance: |
| 33 | + |
| 34 | +```typescript |
| 35 | +const fusion = await mockFramework<[InvoiceModule]>((configurator) => { |
| 36 | + enableInvoicesMock(configurator, { total: 42 }); |
| 37 | +}); |
| 38 | + |
| 39 | +await fusion.modules.invoices.getInvoice('inv-1'); // typed, no cast |
| 40 | +``` |
| 41 | + |
| 42 | +### Giving your module the same accessor as `.msal` and `.serviceDiscovery` |
| 43 | + |
| 44 | +`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. |
| 45 | + |
| 46 | +Subclass `FrameworkMockConfigurator` and use the protected `_pin`/`_getConfig` pair it exposes for exactly this — the same mechanism `.msal` and `.serviceDiscovery` are built from: |
| 47 | + |
| 48 | +```typescript |
| 49 | +class AppMockConfigurator extends FrameworkMockConfigurator<[InvoiceModule]> { |
| 50 | + constructor() { |
| 51 | + super(); |
| 52 | + this._pin(invoiceMockModule); |
| 53 | + } |
| 54 | + |
| 55 | + get invoices(): InvoiceMockConfigurator { |
| 56 | + return this._getConfig('invoices'); |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +`_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. |
| 62 | + |
| 63 | +## What is not covered yet |
| 64 | + |
| 65 | +`.services`, `.context` 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: none of the three modules 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. |
| 66 | + |
| 67 | +`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. |
| 68 | + |
| 69 | +## `.http` |
| 70 | + |
| 71 | +`.http` is backed by `HttpMockConfigurator` (`@equinor/fusion-framework-module-http/mock`): every named client it builds answers requests from registered route handlers instead of the network, so a test needs no locally running server. |
| 72 | + |
| 73 | +```typescript |
| 74 | +configurator.http.configureClient('catalog', { baseUri: 'https://api.example.com' }); |
| 75 | +configurator.http.get('/items', () => Response.json([{ id: 1 }])); |
| 76 | + |
| 77 | +const items = await fusion.modules.http.createClient('catalog').json('/items'); |
| 78 | +``` |
| 79 | + |
| 80 | +Route handlers are Fetch-standard middleware — `(request: Request) => Response | undefined | Promise<...>` — matched in registration order, with `undefined` falling through to the next one. Three ways to fill that seam: |
| 81 | + |
| 82 | +- **`.get`/`.post`/`.put`/`.patch`/`.delete`/`.on`** — hand-rolled handlers for a handful of routes. |
| 83 | +- **`fromExpressStyleHandler`** — adapts an Express-style `(req, res)` handler (or a whole framework built from them, like `openapi-backend`) into middleware, so `.use(fromExpressStyleHandler(api.handleRequest))` drops it straight in. |
| 84 | +- **`fromOpenApiMock`** — adapts an `@equinor/fusion-openapi-mock` instance (`createOpenApiMock(document)`), so a real `openapi.json`/`openapi.yaml` fakes every response with no handlers written at all until an edge case needs overriding. |
| 85 | + |
| 86 | +All three are exported from `@equinor/fusion-framework-module-http/mock`. |
0 commit comments