A hook sets a response header after delegating and its isolated test is green, yet the header never reaches clients — what is wrong with the fixture?
answer
- the stub is not a null object
- what the stand-in never does
- committed response drops late headers
- stub writes before returning
- one stub variant per downstream behaviour
basics
~20 sThe stand-in for the rest of the chain returns without writing anything, so the hook's late header write always succeeds. Make it write a status and body before returning, as the real downstream does, and the test reproduces the loss.
solid answer
~40 sThe default recording stub does nothing but record and return, so nothing has been written when the hook's outbound half runs, and a header set there lands harmlessly. The real downstream writes a status and body, and once the first bytes are flushed the response is **committed**: headers set after that point are dropped or rejected, which is the production bug. The fix is to treat the stub's behaviour as part of the fixture — have it write a status and body before returning, so the response is committed when the hook resumes — and then assert the header is missing or that the write raised. Keep a family of stubs, one per downstream behaviour the hook must cope with, rather than a single do-nothing constant.
go deeper
Remember that the stand-in for the rest of the chain does nothing by default. If the hook works after delegating, the test has to make that stand-in act like the real downstream.
Explain committing: once the first bytes are flushed the status and headers are already on the wire, so a later header write is dropped or rejected.
Show the repair as fixture design — a stub per downstream behaviour, each named in its test — and the habit of asking what the stand-in never does.
Own the boundary: a stand-in that keeps growing becomes an unmaintained second framework, so decide how much fidelity is bought here and what has to be proved elsewhere.
This is the failure mode that makes people distrust isolated hook tests, and it is not a fault in the technique — it is a fault in one specific fixture decision: treating the stand-in for the rest of the chain as a constant instead of as a thing with behaviour. ## Why the green test is lying A hook that works on the outbound side does its job *after* the continuation returns. In the test, the continuation is a recorder whose entire behaviour is 'note the call, return a minimal value'. Nothing has been written to the response when the hook resumes, so a header set at that moment is simply stored, and the assertion finds it. In production the element that follows is not a recorder. It writes a status, writes headers, and starts writing a body — and at the moment the first bytes are flushed to the client the response is **committed**: the status line and headers are already on the wire and cannot be changed. Depending on the framework a later header write is silently discarded, raises, or is applied to an object nobody will ever send. The hook's outbound half runs, sees no error it recognises, and the header never reaches the client. The fixture and production differ on a single axis — *had the response been committed when the hook resumed?* — and the test picked the value of that axis by accident. ## Making the stub behave like the downstream The repair is to stop thinking of the stand-in as a null object: 1. **Give it the behaviour under discussion.** For this bug, the stub writes a status and a body before returning, so the response is committed by the time the hook's outbound half runs. 2. **Assert the real outcome.** Either the header is absent from the response, or the attempt to set it raises. Whichever it is, the test now goes red for the same reason production is broken. 3. **Fix the hook, and let the test turn green for the right reason.** Outbound header work has to happen before anything downstream can commit — set it on the way in, or use whatever mechanism the framework offers for work that must run before the response is flushed. ## The generalisation: the stub is a fixture parameter Once the stand-in has behaviour, it becomes one more axis of the test matrix. Useful variants: | Stub behaviour | What the hook's outbound half must cope with | | --- | --- | | Returns without writing anything | the uncommitted case; late header and body edits still apply | | Writes a status and body, then returns | the committed case; late edits are lost or rejected | | Returns a particular status, such as a client error | outbound logic that branches on the downstream outcome | | Writes a streamed or chunked body | outbound logic that assumes a fully buffered response | | Consumes the request body before returning | outbound logic that tries to read a body that is now exhausted | Each row is its own test with its own name. That is the discipline: the stand-in's behaviour is named in the test's title, so a reader knows which downstream world the assertion belongs to. ## How to notice this class of fixture fault - **A hook that does outbound work has at least two tests.** If it only has the uncommitted one, the missing one is not an oversight — it is the interesting one. - **Ask what the stand-in does not do.** It does not commit, does not consume the body, does not take time, does not fail. Every 'does not' is a production behaviour that has been assumed away, and each is a candidate test. - **Watch for assertions that would pass against an empty implementation.** If the hook's outbound half were deleted entirely, would the test still be green? If so it is asserting on the wrong thing. - **Let production teach the fixture.** When an incident is traced to a hook, the durable fix is not only the code change but the stub variant that would have caught it, added in the same change. ## What this does not mean It does not mean the stand-in should grow into a re-implementation of the framework. A stub that gradually acquires routing, negotiation, and buffering is a second framework that nobody maintains, and its divergence from the real one is a source of false confidence in both directions. The rule of thumb is narrow: give the stand-in the *one* behaviour the test is about, keep it a few lines long, and name the test after that behaviour. Everything beyond the hook's own inputs and outputs — what the surrounding stack does with the response once the chain returns — is outside what this kind of test can observe at all, and pretending otherwise is how a stub turns into a liability.
- Where should the header be set so it survives a committed response?On the way in, before the hook delegates, so it is already on the response when anything downstream flushes. Where the value is only known afterwards, it has to use whatever pre-flush callback the framework provides rather than a plain write after delegating.
- How many stub variants is it reasonable to keep?One per downstream behaviour that changes the hook's outbound path: committed versus uncommitted, an error status, a consumed request body, a streamed response. Each is a few lines and a named test; anything more elaborate is re-implementing the framework.
- What routine question exposes this class of fixture fault before production does?Ask what the stand-in never does — never commits, never fails, never takes time, never consumes the body. Each 'never' is a production behaviour assumed away, and each is a candidate for a second test.
saying these in an interview costs you the question
- Assuming a stand-in that returns immediately behaves like the real downstream.
- Believing response headers can be changed at any point before the chain returns.
- Concluding the hook is correct because its isolated test is green.
- Using one do-nothing stub for every test of a hook's outbound half.
- Growing the stub into a re-implementation of the framework's response handling.
- Assuming every framework buffers the whole response until the chain unwinds.