skip to content

What does a web-layer slice test that boots the router and hooks with services replaced prove that a direct handler call cannot?

level: middleimportance: must knowfreq 70%

answer

  1. the subject becomes a response
  2. declarations get read at last
  3. route, bind, hooks, serialise
  4. cost is per configuration, not per test
  5. nothing below the replaced collaborator

basics

~20 s

A slice runs the real routing, binding, hook chain and response mapping, so it proves the route matches, the request binds, and the status, headers and serialised body match the contract. A direct call touches none of that.

solid answer

~50 s

The slice boots the parts of the framework that turn an HTTP request into a handler invocation and a handler result into a response, while the collaborators underneath the handler are replaced with test doubles. That means the assertions move from a returned value to a response: the route matched this path and method, the body and query values bound to the arguments, the hooks in the chain ran in order, the status code is the agreed one, the headers are present, and the payload serialises under the field names the contract promises. It costs a framework startup rather than a function call, so a suite reuses one booted slice across many tests. What it still does not prove is anything below the replaced collaborators — including whether the real ones would have been wired in at all.

go deeper

for a junior

Focus on the change of subject: the assertion is now a response with a status, headers and a body, not a value returned from a function. That is why the route and the payload shape finally get tested.

for a middle

Name the steps the slice restores — matching, binding, validation, hooks, response mapping — and give one concrete fault per step. That list is the expected answer at this level.

for a senior

Bring the cost model: one start-up per configuration, amortised across tests, so configuration churn is what makes suites slow. Also state the blind spot the replacement creates.

for a principal

Argue about where the tier sits in the whole suite's budget: it is the cheapest place the request is genuinely a request, so it should carry contract coverage while the scarce full-stack runs prove the graph itself.

## What a slice actually boots A web-layer **slice** is a partial start-up of the framework: enough of it to serve a request end to end through the HTTP machinery, and no more. Typically that means the route table, the request-binding layer, the hook or middleware chain, error translation, and the response writer — while the services the handler depends on are replaced with test doubles. Frameworks differ in how the boundary is drawn: some let you name a single handler and load only what it needs, others start the whole application and swap components inside it, and the narrower the slice, the faster it starts and the more of the real chain it omits. The test then feeds a request into that machinery and asserts on the **response**, not on a returned value. That single change of subject is what buys the extra evidence. ## What the slice proves and the direct call cannot - **Route matching** — this path template and this method reach this handler, and win against any overlapping route. - **Binding** — a path value, a query value, a header, and a body payload arrive as the arguments the handler declares, with the right types and the right treatment of absent values. - **Declarative validation** — rules applied before the body is entered actually reject what they claim to reject, producing the rejection response rather than an entry into the handler. - **The hook chain** — the hooks configured for this route run, in order, and contribute what they are supposed to contribute. - **Status mapping** — the handler's result becomes the intended status code, including the difference between a created resource, an empty result and a domain error. - **Headers** — content type, caching, location, and anything a hook attaches. - **Serialisation** — the payload's field names, nesting and omissions as a client would parse them, which is the part most likely to drift from the contract silently. A compact way to say it: the direct call tests the code you **wrote**, and the slice tests the code you **declared**. Route templates, binding rules, validation constraints and response metadata are declarations, and only something that reads them can prove them. ## The price | Tier | What starts | Typical assertions | Dominant cost | |---|---|---|---| | Direct handler call | Nothing | Returned value, raised error | A function call | | Router-and-hooks slice | Routing, binding, hooks, response writing | Status, headers, serialised body | One framework start per configuration | | Full stack on real wiring | The whole application graph | The same, plus that the real graph builds | A full start, plus real resources | The slice's cost is **fixed per configuration, not per test**. One booted slice usually serves every test in a class or suite, so the second hundred tests are nearly free while the first one is not. That is why churn in the slice's configuration — a different set of replaced services per test — is the thing that actually makes a suite slow: each distinct configuration is another start-up. ## What the slice still cannot see The replacement that makes the slice fast is also its blind spot. Below the double there is nothing: no real implementation, no configuration it would have read, and crucially **no proof that a real implementation is registered at all**. A slice is perfectly happy to supply a collaborator the real application has never been able to build. It also proves nothing about behaviour that only appears when several components interact for real — a transaction that must span two of them, a serialiser configured globally at start-up in a way the slice did not reproduce. ## Choosing the assertions 1. Assert the **contract**, not the plumbing: status, the headers a client acts on, and the payload's shape. Those are what break other people's code. 2. Assert **one** route's behaviour per test, so a failure names the route. 3. Keep the number of distinct slice configurations small, because each one is a start-up. 4. Resist re-testing branch logic here; those cases belong one tier down, where they cost nothing. The slice is the tier that earns its keep on most services: it is the cheapest place where the request looks like a request and the response looks like a response, which is exactly where HTTP-layer defects live.

  • Why does the number of distinct slice configurations matter more than the number of slice tests?
    A slice's cost is a framework start-up, and the start is amortised over every test sharing that configuration. Two hundred tests on one configuration pay one start; twenty tests each demanding a different set of replaced services pay twenty. Suite runtime therefore tracks configuration churn, not test count.
  • Which assertion in a slice test most often catches a real contract break?
    The serialised body's shape. Field naming, nesting and omission rules are decided by serialisation configuration rather than by handler code, so they drift without any handler change. Status codes and headers come next; the returned object's own fields are the weakest assertion because the direct-call tier already covers them.
  • Can a slice test prove that declarative validation rejects a bad request?
    Yes, and it is one of the clearest reasons to use the tier. The rules run before the handler is entered, so only a routed request exercises them. The test sends a payload violating a constraint and asserts the rejection response, proving both that the rule is attached and that the failure is translated into the agreed status and body.

Testing the handler alone is like working a light switch held in your hand. The slice screws it into the real wall plate and wiring of the room, but still feeds it from a bench power supply instead of the building's mains.

saying these in an interview costs you the question

  • Treats a slice test as proof the application starts
  • Asserts on the handler's return value instead of the response
  • Re-tests every branch at slice level instead of one tier down
  • Creates a new slice configuration per test, then blames the framework for slow tests
  • Believes a replaced collaborator implies a real one is registered