skip to content

Test Slice Boundaries

A handler called directly, a slice of router and hooks with services replaced, or a full stack on real wiring: what each asserts and costs. Asked because a replaced service hides wiring faults.

on this pageshow

questions

4

When you test a web handler by calling it directly as a plain function, what is proved and what is not?

level: juniorimportance: must knowfreq 62%

answer

  1. no framework is running
  2. the test builds the arguments
  3. logic yes, wiring no
  4. declarations never get read
  5. route match and binding untested

basics

~10 s

It proves only the handler body: its branches, its return value, its exceptions. Route matching, request binding, the hook chain, response serialisation and status or header mapping never run, because no framework was started.

solid answer

~50 s

Calling the handler directly makes it an ordinary function test. The test builds the arguments itself — a request object, decoded body, path values — so the framework's job of turning a real HTTP request into those arguments is assumed, not exercised. What you get is fast, precise feedback on logic: branches, validation inside the body, how collaborators are called, what value comes back. What you do not get is anything the framework contributes: whether the route is registered under the right path template and method, whether a body field actually binds, whether the returned value maps to the intended status code, whether headers are set, whether serialisation emits the field names a client expects. So this tier is where branch combinatorics belong, and it is never on its own evidence that an endpoint works.

go deeper

for a junior

Remember the one-line split: a direct call tests the function, not the endpoint. Say out loud what did not run — routing, binding, hooks, serialisation — and you have answered the question.

for a middle

Explain where the arguments came from. The test constructed them, so the mapping from a real request to those arguments is assumed; name the concrete faults that assumption hides, such as a mismatched body field name.

for a senior

Show how you use the tier deliberately: branch combinatorics here, one routed contract test per route, and scepticism about coverage numbers that count declarations as covered code.

for a principal

Frame it as cost against evidence. This tier is nearly free and scales with cases, so it should carry the volume, while the tiers that boot framework machinery are scarce and should be spent on the faults only they can see.

## What "calling the handler directly" means In most server-side web frameworks a **handler** is the function the framework invokes once it has decided that an incoming request belongs to a particular route. Testing it directly means the test calls that function itself: it constructs whatever the function declares — a request value, an already-decoded body object, path or query values, some context — and asserts on the value returned or the exception raised. Nothing framework-shaped happens. No route table is built, no hook or middleware chain runs, no body is decoded from bytes, no response is serialised, and no status line is produced. The handler is a function, and the test treats it as one. ## What it genuinely proves - **Branch logic**: every conditional path through the body, including the ones that are awkward to trigger over HTTP. - **Domain-level error behaviour**: that a missing entity leads to whatever the body does about it — raising, returning an error value, or returning an empty result. - **Collaboration**: that the handler calls its dependencies with the arguments you expect, in the order you expect, and does not call them when it should not. - **The returned value's shape**, as the handler sees it — not as the client sees it, which is a different thing. Because there is no startup, the cost is the cost of a function call. That property is the whole point of the tier: you can afford hundreds of cases, run them on every keystroke, and keep them deterministic. ## What it assumes rather than tests Everything the framework would have done sits outside the call, and each of those steps is a real source of defects: 1. **Route registration** — the path template, the method, and which of several overlapping routes wins. 2. **Binding** — decoding the body, mapping query and path values, type conversion, defaults for absent values, and whatever declarative validation runs before the body is entered. 3. **The hook chain** — anything the chain contributes or short-circuits before and after the handler. 4. **Response mapping** — how the returned value becomes a status code, which headers are attached, and how the payload is serialised, including field naming and omitted nulls. 5. **Wiring** — the handler under test received the collaborators the *test* chose, not the ones a real graph would have supplied. | Fault | Caught by a direct handler call? | |---|---| | Wrong branch or comparison inside the body | Yes | | Exception thrown for an invalid argument | Yes | | Path template registered with the wrong parameter name | No | | Route declared for the wrong method | No | | Body field that never binds because of a name mismatch | No | | Status code derived from metadata declared outside the body | No | | A response header that a hook was supposed to add | No | | Serialised payload missing or renaming a field | No | | Collaborator that has no registration in the real graph | No | ## Where the tier is the right tool Use it wherever the interesting variation is **inside** the function and has nothing to do with HTTP. A handler with eight validation branches does not need eight routed tests; it needs eight direct calls and one routed test proving the route exists and answers with the agreed status and body. Frameworks differ in how easy this is: where a handler is a plain function taking plain values, the direct call is trivial, while where a handler is written against a framework-owned request and response object, the test must construct or fake those objects, and the more of the framework you have to imitate to make the call work, the less the result is worth. ## How to keep the gap honest - Treat a green direct-call suite as evidence about **logic**, never as evidence that the endpoint is reachable. - Keep at least one routed test per route that asserts the contract a client depends on: status, the headers that matter, and the serialised body's shape. - Be suspicious of coverage reports here. Lines inside the handler are covered; the declarations that *register* it, the binding rules, and the response mapping are configuration the report happily shows as covered while nothing ever executed them against a real request. - When you find yourself building an elaborate fake request just to call the function, that is the signal to move the test up a tier rather than to build a better fake. The summary a strong candidate gives is short: a direct call tests the code you wrote, and a route is mostly code you *declared*. Declarations are only tested by something that reads them.

  • Why can a coverage report look healthy while the route itself is broken?
    Coverage records lines executed inside the handler body. A path template, a method declaration, a binding rule and a response-mapping declaration are configuration read by the framework at startup, and a direct call never starts it. The body can be fully covered while nothing has ever proved the route matches or that its payload serialises as agreed.
  • When does a direct handler call stop being worth writing?
    When the arguments are hard to build honestly. If making the call work means imitating a large framework-owned request or context object, the test is now asserting against your imitation. At that point the same case costs less, and means more, as a routed test over the real request pipeline.
  • Does a direct call prove anything about the status code the client receives?
    Only when the handler itself constructs the whole response, and even then serialisation is untested. Where the status is derived from the returned type or from declared metadata, that mapping is framework work that the direct call skips entirely, so the status is an assumption until something routes a request.

saying these in an interview costs you the question

  • Claims a green handler test proves the endpoint is reachable
  • Assumes the framework binds a request exactly as the test constructed it
  • Reports handler-body coverage as HTTP-layer coverage
  • Thinks status and headers declared outside the body are covered
  • Builds an elaborate fake request instead of routing a real one
open as a page

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%

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.

open as a page

Your web-layer slice tests are green, yet the deployed route fails on first real use — how does that happen and what do you change?

level: seniorimportance: should knowfreq 54%

basics

~20 s

The slice supplied a test double where production must build a real collaborator, so that collaborator's construction, configuration and registration were never exercised. Add a test that boots the whole application on real wiring, faking only true externals.

open as a page

How would you split HTTP-layer coverage across handler-only, slice and full-stack tests for a service with hundreds of routes?

level: principalimportance: nice to knowfreq 38%

basics

~20 s

Push case volume down to handler calls, which cost nothing; assert each route's contract once in a slice, whose cost is per configuration; spend the full stack on proving the real graph builds. Steer by which faults escape.

open as a page