Your SDK’s test suite starts a server on a port, seeds it over HTTP, and fails on a developer machine where that port is taken. Half the tests exist to check argument handling and never needed a socket at all. The suite takes ninety seconds and the failures are about the harness rather than about the client.
What you get
You will end up with a client that answers its own calls from a map you seeded, so a test exercises the real generated code path with no server and no port. This is for you if you ship a generated SDK whose tests need something listening.
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 and its compiled output is
committed, so the commands here run offline. One thing to know first:
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 | 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 | 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 | 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 | 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.
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.
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.
node demo.mjs
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.
node --test sdk.test.mjs
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
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.
Footnotes
-
The name describes the browser half. The MSW philosophy page 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. ↩︎ Back to text
-
The two rows share a layer. The nock README says it works by overriding Node’s
http.requestfunction, and the registry record for nock 14.0.0 lists@mswjs/interceptorsamong its three dependencies. The package document 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. ↩︎ Back to text