A test suite that reaches a real API fails on somebody else’s deploy, at somebody else’s rate limit, and on the day the office network is slow. It also passes for the wrong reasons: a test that asserts on a 200 cannot tell a working integration from a vendor sandbox that returns 200 for everything you send it.
What you get
You will end up with a suite that cannot reach the network, stubs each call by host and path, and fails when your code makes a request no test expected. This is for you if your tests call out.
Short answer
Call nock.disableNetConnect once for the suite, then declare an interceptor per test with the host, path, and expected request headers. Assert with isDone that the interceptor was used, so a test cannot pass because your code never made the call. Clean interceptors between tests, because one left behind answers a request in a later test.
You will need
Node 22 or later, and code that makes outbound calls. Version 14 of
nock intercepts the global fetch as well as http.request, which
matters because a mock that only covers one of those leaves half a modern codebase unstubbed.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| A local test server | You want no mocking library and full control of the responses | Your code has to accept a base URL, and every test manages a port and a lifecycle | The call is buried in a dependency you cannot point elsewhere |
| MSW | The same handlers should serve browser tests, Node tests and local development | A larger tool with its own handler model, which is more to learn for a backend-only suite | Only Node tests need stubbing |
| nock | Node suites that want per-test stubs and assertions on the request | Interceptors are consumed once, so a retry test needs the stub declared as many times | The code under test talks to a database or a queue rather than HTTP |
| undici MockAgent | You already build requests through an undici dispatcher | It covers undici’s own paths, so a library using node:http directly is untouched | Calls are made by several different clients |
The local server and the interceptor differ in what they can reach. A server needs the code under test to be pointed at it, which a well-designed client allows and a third-party dependency usually does not. An interceptor works at the HTTP layer, so it catches calls from code you cannot configure.
Turn the network off first
The single most valuable line is the one that makes an unstubbed call an error.
before(() => {
// Any request the test suite did not stub is a real network call, and a test
// suite that can reach the internet is a test suite that fails at somebody
// else's deploy. This makes an unstubbed call an error instead.
nock.disableNetConnect()
})
beforeEach(() => nock.cleanAll())
after(() => { nock.cleanAll(); nock.enableNetConnect() })
Without it, a call your stubs do not match goes to the real host. The test then either passes because the vendor answered, or fails with a network error that reads like a bug in your code. With it, the same call fails immediately and names the host it tried to reach.
Cleaning between tests is the other half. An interceptor that a test declared and never used stays armed, and the next test that happens to call the same path gets the previous test’s response. That failure is hard to find because it depends on test order.
Assert on what you sent
A stub is a chance to check the request, not only to supply a response.
test('the stub asserts on the request as well as answering it', async () => {
const scope = nock('https://api.example.com', {
reqheaders: { authorization: 'Bearer sk-test-1' },
})
.get('/v1/invoices/in_2')
.reply(200, { id: 'in_2' })
await fetchInvoice('in_2')
assert.equal(scope.isDone(), true, 'the interceptor was used')
})
The reqheaders option makes the interceptor match only when the header is present and correct. A
request missing it does not match, so it falls through to the blocked network and fails. The header
assertion and the response stub are the same declaration.
isDone is what stops a false pass. Without it a test that never calls the API still succeeds,
because nothing checks that the interceptor was reached, and a refactor that removes the call
entirely leaves the suite green.
Check it worked
Four tests: a stubbed response, an assertion on the request, a 404 the client maps to null, and a call nobody stubbed.
node --test nock.test.mjs
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 228.192563
The fourth test is the one worth reading twice. It asserts that an unstubbed call rejects, which proves the suite is closed rather than assuming it.
Running the same file with the network left on is the way to see the difference. Comment out
disableNetConnect, change one path so the stub no longer matches, and the request leaves the
machine. Whether that fails depends on the network and on the vendor, which is the property a test
suite should never have.
The other thing to check once is the interception surface. A dependency that uses node:http directly, rather than the global fetch, is covered by the same interceptors. That is the reason to prefer an HTTP-layer mock over passing a fake client into your own code.
When it goes wrong
A retry test passes on the first attempt and fails on the second. An interceptor answers one request
and is then spent, so code that retries needs the stub declared for each attempt, or .times(3).
The failure looks like your retry logic misbehaving when the stub ran out instead.
The second failure is a stub that does not match and gives no reason. A path with a query string, a
trailing slash, or a body that differs by whitespace all fail to match, and the visible result is
only that the request escaped. Print nock.pendingMocks() in the failing test, which lists the
interceptors still waiting and the exact shapes they expect.
The third is a suite that stubs so precisely it tests the stub. A test asserting on a fixed response body it wrote itself proves that a constant equals a constant. Keep the stub thin and assert on what your code did with it.
When not to do this
Do not stub the API in every test in the suite. The stubs encode your belief about the vendor, and a suite made only of stubs passes whatever the vendor is doing, which is what a scheduled contract check exists to cover.
Do not use interceptors to test your own service’s endpoints. Those you can call for real, in process, and a stub between two pieces of your own code hides the integration that is worth testing.
Do not leave disableNetConnect out because one test needs the network. Allow that host explicitly
with nock.enableNetConnect(host), so the exception is visible and narrow rather than a suite-wide
gap nobody remembers.
Do not record stubs from production traffic without reading them. A recorded interaction carries whatever headers and identifiers were in flight, and those end up committed to a repository.
Related how-tos
Last verified
Verified 2026-09-06 against Node 22.22.2 and nock 14.0.10. The output block is what the preceding command printed, with the network turned off for the whole run.