# How to run an in-memory mock of your API inside SDK tests

> Build the client in test mode, seed it with records, and let the generated SDK answer its own calls from memory instead of reaching the network.

Source: https://voxgig.com/howto/run-an-in-memory-mock-inside-sdk-tests

- Audience: api-producer
- Level: intermediate
- Languages: typescript, javascript
- Verified: 2026-09-14
- Published: 2026-09-14
- Updated: 2026-09-24

## Short answer

Construct the client with `MetercoSDK.test(seed)` rather than `new MetercoSDK()`, where `seed` holds records keyed by id under each entity. The generated test feature then answers `list`, `load` and `create` from that map, so a test needs no server, no port, and no fixture files. Assert the mode as well, because a client that fell back to live looks identical from outside.

---
## You will need

Node 22 or later, and an SDK generated with the test feature enabled. The sample was generated with
[create-sdkgen](https://www.npmjs.com/package/@voxgig/create-sdkgen) and its compiled output is
committed, so the commands here run offline. One thing to know first:
[apidef](https://github.com/voxgig/apidef) refuses a description with no `servers[0].url`, and says
so rather than guessing.

Voxgig maintains sdkgen. This page compares its test feature with MSW, nock, and a mock server you
run.[^1]

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [MSW at the fetch layer](https://mswjs.io/docs/) | The client uses fetch and you want the same handlers in a browser | Handlers written per endpoint, and a layer below the SDK to keep in step | The SDK offers a mode that skips the transport entirely |
| [A local mock server](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) | You want to exercise headers, status codes and the wire format | A port, a lifecycle, and flakiness on machines where the port is busy | The test is about argument handling rather than the wire |
| [nock at the transport layer](https://github.com/nock/nock) | A client on Node's `http` module, with recordings of real traffic | Interceptors that go stale, and a layer the generated client may not use | The client calls `fetch` rather than `http` |
| [The sdkgen test feature](https://github.com/voxgig/sdkgen) | The SDK is generated and you want no transport in the test at all | A seed shaped the way the mock expects, and behavior to pin per version | You need to assert on headers, retries or status codes |

The dividing line is which layer you replace. MSW and nock replace the transport, so the whole
client sits on top of them and the wire format still matters.[^2] The test feature replaces the request
itself, so nothing is serialized and nothing is parsed, and a test runs in milliseconds.

That is also its limit. A mode that never builds a request cannot tell you the authentication header
was wrong. A suite built only on it passes while the client cannot talk to the service at all. Keep
a small number of tests at the transport layer, or against a sandbox.

## Seed by the record id, and assert the mode

Two things belong in every test that uses it.

```js title="sdk.mjs"
export const seed = () => ({
  entity: {
    meter: {
      mtr_8f2: { id: 'mtr_8f2', serial: 'SN-40199', state: 'installed' },
      mtr_31a: { id: 'mtr_31a', serial: 'SN-40200', state: 'retired' },
    },
  },
})
```

Each record carries an `id`, and the map key matches it. That is what the mock selects on, and the
next section shows what happens when a caller addresses a record some other way.

Assert `_mode` in at least one test. A client that was meant to be in test mode and is not looks
exactly the same until something reaches the network, which on a developer machine may even succeed.

## Address records the way the client does

The mock matches on the record's own fields, not on the route's path parameters.

```js title="demo.mjs"
await show("load({ id })", async () => (await sdk.Meter().load({ id: 'mtr_8f2' })).data().serial)
await show("load({ meterId })", async () => (await sdk.Meter().load({ meterId: 'mtr_8f2' })).data().serial)
```

The description's path is `/meters/{meterId}`, so passing `meterId` looks right and is not what the
generated client asks for. The entity operations take the record's own key, which is what the README
the generator writes shows.

## Check it worked

Run both shapes against a seeded client.

```bash
node demo.mjs
```

```text output
test client mode               test
ordinary client mode           live
list                           ["SN-40200","SN-40199"]
load({ id })                   "SN-40199"
load({ meterId })              "SN-40200"
create, then list again        ["SN-40200","SN-40199","SN-40201"]
load({ id: 'mtr_nope' })       error: MetercoSDK: load: request: 404: Not found
```

Line five is the finding worth carrying away. `load({ meterId })` returned a record, and it is the
wrong one: the mock matched nothing on that field and answered with whatever was first. It does not
raise, so a test written that way passes while asserting something untrue. Address records by `id`,
and assert on a field that differs between your seeded records so a wrong match fails.

The rest behaves as you would want. A create is readable afterwards, an id nobody seeded raises a
404 from the mock rather than a network error, and the ordinary client reports `live` so a missed
test-mode construction is visible.

The list order is the seed's insertion order reversed, which is worth knowing before you assert on
it. Sort in the test, or compare as a set, and the suite survives a seed somebody reorders.

```bash
node --test sdk.test.mjs
```

```text output
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 170.380797
```

## When it goes wrong

The mock answers nothing and every call is a 404. The seed is keyed under the wrong entity name. Use
the name the generated client uses, which comes from the model rather than from the path segment.

A create vanishes. The client was built with no seed at all, so each operation reads a fresh empty
map. Pass a seed object, even an empty one per entity, and the store persists for that client.

Two tests interfere. They share one seeded client. Build a client per test, which costs nothing
because there is no server to start and no port to wait for.

The suite passes and the released SDK cannot authenticate. Nothing in test mode builds a request.
Add a handful of transport-level tests, and run them against a sandbox before a release. Three is
usually enough: one authenticated read, one write, and one deliberate failure.

## When not to do this

Do not test retries, timeouts or rate limit handling with the test feature. Those live in the
transport, and a mode that skips the transport cannot exercise them. Use nock, MSW or a mock server
for that layer.

Do not seed a fixture large enough to need its own maintenance. A mock with two hundred records is a
second database, and the tests that use it start depending on data nobody can explain.

Do not rely on sdkgen's test feature behaving the same across versions without pinning it. The
`load` result shown here is version-specific, and the versions are recorded in this page's
frontmatter. Run the same check in your own project before your tests depend on it.

## Related how-tos

- [Stub outbound HTTP calls in Node tests with nock](/howto/stub-outbound-node-http-calls-with-nock)

- [Run an MCP server against a mock API while you build tools](/howto/run-an-mcp-server-against-a-mock-api)

## Last verified

Verified 2026-09-14 against Node 22.22.2, @voxgig/create-sdkgen 0.21.0 and @voxgig/sdkgen 4.17.1.
Both output blocks are what the preceding command printed.

[^1]: The name describes the browser half. [The MSW philosophy page](https://mswjs.io/docs/philosophy)
says it intercepts traffic by using a designated Service Worker in the browser, or by implementing
custom request interception algorithms in Node.js. A Service Worker is a browser API, and the
second clause is what runs in a test suite. Mock Service Worker in Node is therefore a mock, of a
service, without the worker.

[^2]: The two rows share a layer. The nock README says it works
[by overriding Node's `http.request` function](https://github.com/nock/nock#how-does-it-work), and
[the registry record for nock 14.0.0](https://registry.npmjs.org/nock/14.0.0) lists
`@mswjs/interceptors` among its three dependencies. The
[package document](https://registry.npmjs.org/nock) dates that release to January 2025. One of
the two transport-layer rows in the table is now built on interception code published by the
other.