node:test mock.method on fetch: test a Sume status poll offline

Node 26 goes LTS this month. Test a Sume job status poll with node:test and mock.method on fetch: four cases, no network, no key, no third-party test library.

4 min readSume
All posts

Use the built-in test runner: import test, { mock } from "node:test", replace globalThis.fetch with mock.method, return a real Response object, and assert on what your poll function decides. That tests the logic that reads a Sume job status without a network, an API key or a test dependency. Run it with node --test.

The timing is practical. Node 26 moves to LTS in October 2026 according to the Node.js release schedule, so many teams are about to touch their runtime matrix. A poll function with four fast offline tests is a cheap guard for that upgrade. We ran the file below on Node 22.14; it uses only node:test, node:assert and global fetch.

What the tests pin down

The Sume jobs docs describe GET /v1/jobs/{id}/status returning terminal and next_poll_after_seconds, and the latter is null once the job is terminal. The sample function turns that into a decision. The tests then cover the cases that usually regress: a finished job, a queued job with a hint, the request headers, and a non-2xx answer.

Cases to cover for a status poll (read 2026-10-08)
CaseFake responseExpected decision
Finishedterminal true, next_poll_after_seconds nullstop polling
Queuedterminal false, next_poll_after_seconds 4wait 4 seconds
Authany 200one authorization header, never two
Rate limitedHTTP 429throw, so the caller can read retry-after

Steps

  • Export the function so the test file can import it, and give it a base URL argument.
  • Call mock.method(globalThis, "fetch", async () => new Response(...)) at the top of each test.
  • Build the fake body in the documented envelope: { data: { ... } }.
  • Assert on the decision and, for the header test, on mock.calls for the last call.
  • Run node --test p18.mjs locally and in CI.

Test file

The auth test matters because the docs say a request must carry one credential, either Authorization: Bearer or x-api-key, not both.

import test, { mock } from "node:test";
import assert from "node:assert/strict";

export async function pollOnce(base, key, id) {
  const r = await fetch(`${base}/v1/jobs/${id}/status`, { headers: { authorization: `Bearer ${key}` } });
  if (!r.ok) throw new Error(`status ${r.status}`);
  const { data } = await r.json();
  return data.terminal ? "done" : `wait ${data.next_poll_after_seconds ?? 2}`;
}
const reply = (data, status = 200) => new Response(JSON.stringify({ data }), { status });

test("terminal job", async () => {
  mock.method(globalThis, "fetch", async () => reply({ terminal: true }));
  assert.equal(await pollOnce("http://x", "k", "j"), "done");
});
test("queued job honors the hint", async () => {
  mock.method(globalThis, "fetch", async () => reply({ terminal: false, next_poll_after_seconds: 4 }));
  assert.equal(await pollOnce("http://x", "k", "j"), "wait 4");
});
test("sends one auth header", async () => {
  const f = mock.method(globalThis, "fetch", async () => reply({ terminal: true }));
  await pollOnce("http://x", "k", "j");
  assert.deepEqual(Object.keys(f.mock.calls.at(-1).arguments[1].headers), ["authorization"]);
});
test("non-2xx throws", async () => {
  mock.method(globalThis, "fetch", async () => reply({}, 429));
  await assert.rejects(pollOnce("http://x", "k", "j"), /status 429/);
});

Mistakes that make these tests lie

Three habits weaken a mocked poll test. First, returning a plain object instead of a Response, which skips the ok and json() behavior your code really depends on. Second, forgetting to restore the mock, so a later test sees the previous fake; the runner's mock.restoreAll() in an afterEach hook fixes that if you add more files. Third, asserting on the whole request instead of the pieces you care about, which breaks whenever you add a harmless header.

Keep each fixture tiny and shaped like the documented envelope. If the API adds a field, your tests should still pass; if it renames terminal, your smoke call should catch it. That split, offline logic tests plus one live key check, keeps CI fast and honest.

What Sume does not do

Sume does not ship a mock server or a fake clock for polling, and these tests do not prove the live API behaves as the fixtures say. Keep one smoke call to GET /v1/me in a separate job, with a real key, for that. We did not run this file on Node 26 itself.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume