skip to content

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%

answer

  1. the framework builds the handler, not you
  2. change the registration, not the object
  3. amend the input to the graph build
  4. everything resolved from that graph sees it

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.

solid answer

~50 s

A framework-booted test does not construct the handler; the container does, resolving each dependency from a set of registrations. So the lever is the registration, not the object: the test contributes a registration for the same boundary that points at a stand-in, and the graph is built from the amended set. Everything resolved from that graph then gets the stand-in — the handler the router invokes, a middleware sharing the same collaborator, a background component. Declaring the stand-in in test-local configuration, in a wiring module the test class asks for, or in a registration conditioned on a test profile are all ways of saying the same thing. Building a stand-in and passing it to an object you construct yourself is a different, narrower technique: it proves something about that object, but the application the test sends requests through is untouched.

go deeper

for a junior

Remember the one-line rule: the framework builds the handler, so you change the registration it builds from, not an object you constructed. Be able to name where your test declares that replacement.

for a middle

Explain the pipeline — registrations in, object graph out — and why amending the input is the only lever that reaches components the framework constructs. Cover lifetime, since replacing a per-request binding with a shared object changes behaviour.

for a senior

Show that you check the replacement was actually exercised, and that you know what it leaves untouched: other startup work, routing, settings. Talk about how the replacement is scoped so it does not reach tests that never asked for it.

for a principal

Frame each override as a piece of the real graph the suite stops exercising. Be ready to say how many replacements a codebase should tolerate before the coupling they paper over is the thing to fix.

## What a wiring override actually replaces A framework-managed application is assembled from **registrations**: statements of the form "for this boundary, use this implementation, with this lifetime". At startup a container reads the registrations and builds an **object graph** — request handlers, the middleware chain, scheduled and background components — resolving every dependency each of them declares. The important consequence for testing is that your test does not construct the handler. The framework does. Setting a field on an object you made yourself has no path to the object the router will invoke. The only leverage a test has over what that handler collaborates with is the set of registrations the container reads before it builds anything. A **wiring override** is an amendment to that set: the test contributes a registration for the same boundary that points at a stand-in, and the graph is built from the amended set. Everything resolved from that graph receives the stand-in — the handler, a middleware that depends on the same boundary, a component started at boot — because they all resolve from the same registrations. ## How the three common techniques differ | technique | who sees the stand-in | works for a framework-built handler | main risk | |---|---|---|---| | construct the object under test yourself and pass the stand-in in | only that one instance | no | the graph that actually serves the test's requests is untouched | | contribute a replacement registration before the graph is built | everything resolved from that graph | yes | the replacement reaches other tests that share the graph | | mutate the registry after the application has started | later lookups only, if the framework re-resolves | partly, and unpredictably | consumers already holding a reference keep the real collaborator | The middle row is what "overriding wiring" means, and the other two are the mistakes it is usually contrasted with. ## Where the replacement is declared The declaration takes a few recognisable shapes, and frameworks differ over which they offer: - **Test-local configuration** merged over the application's own, adding or replacing one registration. - **A wiring module the test class names**, holding the stand-ins that whole group of tests wants. - **A per-test declaration the harness reads** and turns into an amendment before it boots. - **A registration conditioned on an active test profile**, so the real one is simply not a candidate for that run. All of these share one property: the harness reads them **before** it asks the container to build the graph. That ordering is the whole mechanism, not an implementation detail — a registration that arrives after the graph exists has missed its moment for anything already constructed. ## What an override does not change It is easy to over-read what a swap buys you: - Registrations you did not touch still run, so work other components do at startup still happens. - The routing table, the middleware order and the settings the application read stay exactly as the booted configuration says. - If nothing on the exercised path depends on the boundary you replaced, the override changes nothing and the test still passes — for the wrong reason. Assert that the stand-in was actually reached, not merely that the response was 200. - Lifetime is part of a registration. Replacing a per-request implementation with one shared object changes how many instances exist, and any state the stand-in accumulates now spans requests. ## A check worth running once After wiring a replacement, prove it landed before trusting anything else the test reports: resolve the boundary and assert on the object you got back, or assert that the stand-in recorded the call you expected. Two minutes there save an afternoon spent debugging a test that was exercising the real collaborator the whole time, and that assertion is the one that fails first when somebody later moves the registration into configuration this test no longer reads. ## What interviewers listen for The answer they want is "I change what the container resolves, before it resolves it". Candidates who have only written tests that call a function directly tend to reach for constructing the collaborator themselves and are surprised that the request still hit the real dependency; candidates who have only ever copied a test template tend to describe the syntax without being able to say what it amends. Saying the mechanism — registrations in, graph out, so amend the input — covers every framework's spelling of it and is what makes the follow-ups (when it must happen, how long it lasts, who else sees it) answerable.

  • How do you tell whether the stand-in was actually used, rather than the route simply returning a success status?
    Assert on the stand-in itself: that it recorded the call you expected, with the arguments you expected, or that a value only it can produce appears in the response. A green status proves the route matched and the handler ran, not that your replacement sat on the exercised path.
  • Does replacing a boundary's registration stop the real implementation's startup work from happening?
    Only the work that implementation does when it is constructed. Anything registered separately — a pool opened by another component, a health probe, a listener started at boot — still runs, because you amended one registration, not the module around it. If that startup work is what makes the test slow or flaky, it needs its own replacement.
  • What changes if the boundary you replace was registered per request and your stand-in is a single shared object?
    The number of instances changes, and with it the lifetime of any state. A recording stand-in shared across requests accumulates calls from all of them, so assertions like "was called once" start depending on how many requests the test sent. Either match the original lifetime or reset the stand-in between requests.

saying these in an interview costs you the question

  • Sets a field on one instance and expects framework-built handlers to change too
  • Assumes replacing one binding also disables everything else that module starts at boot
  • Believes a replacement contributed after startup still reaches already-built consumers
  • Treats a green response status as proof the stand-in was on the exercised path
  • Leaves the replacement in configuration the whole suite loads without meaning to