ReviewOS

stacks/ts-http

Add test faking/mocking (Http::fake() equivalent: stub responses + assert sent)

#1760
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Summary

There's no first-class way to stub httx responses in tests. Today you either hit the real network (flaky, slow, needs credentials) or hand-roll a globalThis.fetch monkey-patch per test. Laravel's Http::fake() is the reference ergonomic here.

Proposal

A faking layer that intercepts dispatch before the network call and records what was sent, so tests can both stub responses and assert outgoing requests.

Stubbing

import { httx } from '@stacksjs/httx'

// URL-pattern → canned response
httx.fake({
  'api.github.com/*': { status: 200, body: { login: 'octocat' } },
  'api.stripe.com/v1/charges': { status: 402, body: { error: 'card_declined' } },
})

const res = await httx.get<{ login: string }>('https://api.github.com/users/octocat')
// res is Ok(HttxResponse) with the canned body — no network hit
  • A bare httx.fake() (no arg) stubs everything with an empty 200.
  • Pattern matching: glob (*) on URL, or a predicate (req) => boolean.
  • Response shorthand: { status, body, headers }, a function (req) => response for dynamic stubs, or a sequence for "first call returns X, second returns Y":
    httx.fake({ 'api.test/*': httx.sequence([{ status: 500 }, { status: 200, body: ok }]) })
  • Faked responses still flow through the Result<HttxResponse<T>, Error> contract and the <T> generic, so test code reads identically to production code.

Assertions

httx.assertSent(req => req.method === 'POST' && req.url.endsWith('/charges'))
httx.assertSent(req => req.headers.Authorization === `Bearer ${token}`)
httx.assertSentCount(2)
httx.assertNothingSent()
httx.assertNotSent(req => req.url.includes('/admin'))

Recorded requests reuse the existing RequestCompleteRecord shape (dist/types.d.ts:36-48) — method, url, status, headers, body — which the client already builds for onRequestComplete. The faking layer can capture those records directly.

Lifecycle

  • httx.fake(...) enables interception for the current process.
  • httx.restoreFetch() (or auto-reset via a test hook) tears it down.
  • Unmatched requests while faking either pass through (configurable) or throw a clear "no stub matched <url>" — default to throw so tests can't accidentally hit the network.

Why

  • Removes the per-test fetch monkey-patch boilerplate.
  • Makes httx testable in CI without network/credentials.
  • The RequestCompleteRecord plumbing already exists — this is mostly an intercept point + a matcher/recorder, not new request machinery.

Discovered while

Auditing the HTTP client from the Stacks side (stacksjs/stacks#1924). Sibling to the fluent-builder request (#1759).

9 days ago

Implemented and merged to main via #1761 (commit 7cac61d).

Full Http::fake() faking layer, matching the spec here: glob patterns (with METHOD prefix + protocol-less matching), bare fake() -> empty 200, function responders, httx.sequence([...]) for ordered responses, assertSent / assertNotSent / assertSentCount / assertNothingSent, restoreFetch, strict-by-default (HttxFakeUnmatchedError) with opt-in passthrough. The intercept synthesizes a real Response so the client's parse path is unchanged. 28 hermetic tests (no network) pass.

Sign in to comment on this issue.