skip to content

Testing the HTTP Layer

How much framework a web-layer test boots: a handler alone, router and hooks with services swapped, or the full stack over a socket. Interviewers probe it because the wrong slice proves nothing.

on this pageshow

explore

questions

24

Why should the test suite for a protected route include a request that carries no credentials at all?

level: juniorimportance: must knowfreq 62%

answer

  1. the happy path never questions the guard
  2. remove one factor: the credential
  3. who answered, guard or handler?
  4. status plus body plus no side effect

basics

~20 s

A test that always sends a valid credential passes even when the route is not protected at all. The credential-free request is the case that proves a guard is actually wired on that route and answers before the handler runs.

solid answer

~40 s

A happy-path test drives the route with a caller who is allowed through, so the guard is never asked a question whose answer could differ. Delete the guard and that test still goes green. The credential-free request is the control: it is the same route and the same method with one factor removed, so a rejection can only come from the layer that checks identity. Assert more than the status - assert the error body against the shape the service documents, and assert that the handler's collaborator was never invoked, because a status alone can be produced by an unrelated layer such as the framework's unknown-route fallback when the test's path has a typo.

go deeper

for a junior

Recall the shape of the case: same route, same method, credential removed, and an assertion on the exact status rather than on 'not success'.

for a middle

Explain why the case isolates the guard - one factor changed - and why the error body and a no-side-effect assertion are needed before the result means anything.

for a senior

Show how you keep negative coverage from rotting as routes move between groups, and how you detect a shared test client that attaches identity behind your back.

for a principal

Weigh where this control belongs so it is written once per protected route without becoming ceremony, and how review enforces it on new routes.

## The blind spot in a happy-path test A web-layer test that sends a valid credential and asserts a `200 OK` proves one thing: that a caller who has already been let through gets the documented response. It cannot prove **who is let through**, because the component that makes that decision - the guard, filter, hook, or interceptor that inspects the request before the handler - was never put in a position where it could answer differently. If someone removes the guard from the route, or moves the route out from under the group the guard is attached to, the happy-path test stays green. That is the whole reason interviewers ask this: an authentication requirement that no test can fail is not a requirement, it is a comment. The credential-free request is the **control case**. It is the same path and the same method as the happy path, with exactly one factor removed. When it is rejected, the rejection can only have come from the layer that looks at identity, because nothing else about the request changed. ## What the case actually pins down - **That the guard is attached to this route**, not merely registered somewhere in the application. - **That the guard runs before the handler**, so no work and no side effect happens on behalf of an unidentified caller. - **That the rejection is expressed as a response**, with the status and the body the service contracts for, rather than as a crash or a blank connection. - **That the guard's answer is stable** when the route later moves into or out of a group, because the case fails the moment protection is dropped. ## Assert more than the status A status-only assertion is weak, because several unrelated layers can produce a status in the same family. Three assertions together make the case honest: 1. **Status** - the one the service documents for a request with no identity. 2. **Error body** - the machine-readable code and the field shape the service publishes, so the response is the contracted error and not an accidental one. 3. **No side effect** - the collaborator the handler would have called was not called. With the handler's dependency replaced by a recording stub, this is a direct assertion rather than an inference. | Request the test sends | What a green result proves | What it cannot prove | |---|---|---| | Valid credential | The handler produces the documented success response | That anything is protected | | No credential at all | A guard is wired here and answers before the handler | That credential parsing or verification is correct | | Malformed or expired credential | The parsing and verification branch runs and fails closed | That an unauthenticated caller is refused, since a credential was present | ## How this case is written wrong The most common mistake is asserting only that the status is "not the success status". A typo in the test's path means no route matches, the framework's unknown-route fallback answers, and the assertion still passes - the test is green while the route it names may not even exist. Pin the exact status and the error code from the body. A second mistake is building the request through a shared helper that quietly attaches a credential to everything it sends. The test believes it sent nothing; the client sent an identity. Where a suite has such a helper, the credential-free case must go around it, and it is worth asserting the outbound request as well when the client supports it. A third is treating "no credential" and "bad credential" as the same case. They exercise different branches: one is the absence of any identity to check, the other is the failure of a check that ran. Most frameworks route them to the same rejection status, which is exactly why a test that covers only one of them leaves the other branch unproven. ## Keeping it honest as the route grows When a route later gains a second requirement - say, an additional permission check - the credential-free case keeps its meaning, because it still isolates the earliest decision in the chain. Teams that add only richer positive cases as a feature grows end up with a suite whose negative coverage was written once, at the start, against a much simpler route. The cheap discipline is that every route which gains protection gains its credential-free case in the same change, alongside the positive one, so the two are always written and reviewed together.

  • What does a request carrying a malformed credential prove that a credential-free request does not?
    It exercises the branch where a credential is present and fails its checks - extraction, decoding, signature or expiry. The credential-free case only proves that a request with no identity is refused; it never enters the verification code at all. Both belong in the suite, because a verifier that accepts anything is invisible to the credential-free case.
  • How can a credential-free test pass while the route is still unprotected?
    Two common ways. The test asserts only that the status is not the success status, so a typo in the path lets the unknown-route fallback answer and the assertion holds. Or the shared test client attaches a credential to every request it builds, so the case that claims to send none actually sends one and the handler's own logic produced the status.

saying these in an interview costs you the question

  • Thinks a passing authenticated test proves the route is protected
  • Only tests valid credentials because rejection is the client's problem
  • Asserts a status but never checks the handler stayed out of it
  • Treats a missing credential and a malformed one as one case
  • Assumes protection wired once at startup cannot be dropped later
open as a page

What does an in-process test client do instead of opening a network connection to the application?

level: juniorimportance: must knowfreq 70%

basics

~20 s

An in-process test client builds a request in memory and hands it to the framework's dispatcher, then captures the response as an object. No socket and no port: the call is an ordinary method call in the test process.

open as a page

In a web framework's middleware chain, how do you test one hook in isolation, and what stands in for the rest of the chain?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Call the hook directly with a hand-built request and a recording stub in place of the next element. The stub captures whether it was invoked and with which request, so the test can assert delegation without starting a server.

open as a page

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%

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.

open as a page

In a framework-booted test, how do you make the application resolve a stand-in collaborator instead of the real one?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Contribute a replacement registration for that boundary into the configuration the container reads, so the graph is built with the stand-in. Constructing it by hand affects only your own object, not the one the framework injects into handlers.

open as a page

What does injecting a test principal into a request skip, and when must a test drive the real credential path?

level: middleimportance: must knowfreq 58%

basics

~20 s

Injecting a test principal hands the request an identity directly, skipping credential extraction, decoding, signature and expiry checks. Tests whose subject is one of those steps must send a real credential through the chain instead.

open as a page

What does an in-process test client skip that a real server on an ephemeral port exercises?

level: middleimportance: must knowfreq 62%

basics

~20 s

Everything below the dispatcher: connection setup and reuse, TLS, byte-level request parsing and response framing, listener-enforced timeouts, socket backpressure, and whatever a proxy in front adds. A server bound to an ephemeral port runs all of it.

open as a page

When a middleware short-circuits a request instead of delegating, what should an isolated test assert beyond the response status?

level: middleimportance: must knowfreq 56%

basics

~20 s

Assert that the stub standing in for the next element recorded zero invocations, that the headers the chosen status implies are present, and that no downstream side effect ran. A status alone does not prove the chain stopped.

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

In a web framework, why does swapping a collaborator after startup often leave handlers still using the real one?

level: middleimportance: must knowfreq 55%

basics

~20 s

Because wiring mostly resolves once. Single-instance dependencies are constructed and handed to their consumers while the graph is built, so a later edit to the registry changes only lookups that still happen, not references already injected.

open as a page

Your web-layer test asserts a forbidden outcome on a protected route and passes. What could make it green for the wrong reason?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Several layers can produce that status: an identity that never attached, a missing fixture, an overlapping route, or a broad error mapper flattening an unrelated failure. Pin the body's error code and pair the case with a positive control.

open as a page

How do you test that requests to an unknown path or an unsupported method reach a framework's fallback handlers?

level: middleimportance: should knowfreq 46%

basics

~20 s

Send requests that deliberately miss the routing table: a path no template matches, and a known path with an undeclared method. Assert the exact status for each, the contracted error body, and the header the protocol requires.

open as a page

Why can a streaming-response or timeout test pass under an in-process test client yet fail over a real connection?

level: middleimportance: should knowfreq 46%

basics

~20 s

In-process clients normally collect the whole response and return when the handler finishes, so flush boundaries, time-to-first-byte and backpressure are invisible, and a listener-enforced timeout has no connection to fire on. The assertion passes without the behaviour existing.

open as a page

How would you test the order several middleware run in, using a chain whose terminal element does nothing?

level: middleimportance: should knowfreq 46%

basics

~20 s

Assemble the chain through the application's own registration code, ending it with an element that only returns an empty success, and have each hook append a label before and after delegating. Compare the recorded sequence with the expected one.

open as a page

How does an active test profile make a booted application resolve a fake dependency instead of the real one?

level: middleimportance: should knowfreq 46%

basics

~20 s

A profile is a named run mode that gates registrations. Under it the fake's registration is a candidate and the real one is conditioned out, so the graph builds with the fake — provided the run truly activated that profile.

open as a page

How do you test that an unhandled exception from a handler is turned into the service's contracted error response?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Force the failure at a replaced collaborator so a handler throws something nothing maps, then assert the response: the contracted status, the published error body with its code and correlation identifier, and no exception type, message or stack anywhere in it.

open as a page

An endpoint passes every in-process test but misbehaves behind a TLS-terminating proxy in production - how would you test for that class of fault?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Because an in-process test supplies the connection facts itself, it only confirms your assumption about the deployment. Reproduce over a real listener configured the way production is, with the terminator in front, and assert on what an external client sees.

open as a page

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?

level: seniorimportance: should knowfreq 41%

basics

~20 s

The 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.

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

A test passes alone but later tests in the same run see a stand-in they never registered. What went wrong?

level: seniorimportance: should knowfreq 47%

basics

~20 s

The replacement outlived the test that asked for it — usually a cached graph built with one test's overrides handed to later tests, or a registry mutated at runtime and never restored. Scope replacements to a wiring; reset stand-in state.

open as a page

Your suite boots a new application graph for nearly every test because each declares different overrides. How do you cut that cost?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Harnesses cache a booted graph keyed by its configuration, and each distinct override set is a distinct key, so a unique set per test forces a boot per test. Collapse onto a few shared wirings instead.

open as a page

How would you decide which share of a service's HTTP tests runs in-process versus against a real port?

level: principalimportance: should knowfreq 34%

basics

~20 s

Put the bulk in-process, where cases cost fractions of a millisecond, and reserve wire tests for fault classes only a connection can reveal - framing, TLS, timing, cancellation, proxy trust - one case per class rather than per route.

open as a page

How do you stop test-only wiring from quietly becoming a second application configuration that no longer matches production?

level: principalimportance: should knowfreq 40%

basics

~20 s

Treat each replacement as a piece of the real graph the suite stops exercising. Budget them, review additions like production configuration, and keep at least one tier that boots the unmodified wiring so startup faults still have somewhere to surface.

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