Why can a Mountebank imposter silently send glossary traffic to the real service instead of its own stub?
answer
- unmatched and proxied look identical
- a real reply keeps the test green
- check the tightest predicate first
- proxyOnce makes it intermittent
- give the proxy its own predicate
basics
~20 sA broad Mountebank proxy stub takes every request the specific stubs did not match, so a predicate that grew too tight does not fail loudly. The call is forwarded to the real glossary service and the test still passes.
solid answer
~50 sIn a selective pass-through imposter a broad Mountebank `proxy` stub takes everything the specific stubs did not answer, which means an unmatched request and a deliberately-proxied request are indistinguishable events. So when a specific stub's predicate stops matching - a `deepEquals` on a whole `query` object after the client added a parameter, an `equals` on a header carrying a correlation id, a path the glossary API renamed - nothing fails. The call is forwarded, the real service answers, and the suite goes green while quietly testing a live third party. In WireMock the identical failure is a broad `proxyAllTo(` configuration forwarding a request that a specific mapping no longer matches. Make it loud instead: give the proxy stub its own predicate so it can only take genuinely foreign paths, or point `to` at an unreachable address while you diagnose.
go deeper
Understand that a Mountebank imposter with a broad proxy stub does not reject a request no specific stub matched. It forwards it, so a broken fixture can look like a passing test.
Be able to name the predicates that go stale - a deepEquals over a whole query object, an equals on a correlation header - and explain why the resulting fall-through produces a real reply instead of an error.
Demonstrate the diagnosis: break the proxy's exit, switch to proxyTransparent so the behaviour stops being intermittent, then narrow the specific predicate to the fields the response depends on.
Argue the trade openly. A broad proxy converts every fixture mistake into a silent live call, so decide as a team which traffic may escape, give the proxy a predicate that says so, and put a proxy-disabled run in the pipeline.
## The arrangement and its blind spot Selective pass-through in Mountebank is one imposter holding two kinds of stub: specific stubs whose `predicates` name the translation-glossary paths you are faking, and one broad `proxy` stub with no predicates of its own that takes everything else and forwards it to the real service. The arrangement is deliberate and it is useful, but it has a property nobody chose: **"no specific stub matched" and "this request is meant for the real service" are the same event.** A request that should have been answered by your fixture, and is not, therefore does not fail. It falls into the Mountebank `proxy` stub, goes out to the real glossary service, and comes back with a real status and real terms. The assertions pass. What was a hermetic test is now an integration test against a live third party, and nothing in the run says so. ## What makes a specific stub stop matching - **A predicate that was always too tight.** Mountebank's `deepEquals` on a whole `query` object requires every parameter to be present and equal; the day a client library starts appending a cache-busting parameter, the stub stops matching and the proxy takes over. - **A field that varies by design.** An `equals` predicate on a header carrying a correlation id, a timestamp or a per-run token can never match twice. - **Case and encoding drift.** A client that changes header capitalisation, or starts writing a locale as `en_GB` where the predicate says `en-GB`, misses an `equals` predicate that itself has not changed at all. - **A path the upstream renamed.** The glossary API adds a segment; your specific stub still names the old path, and the new one is forwarded for real. - **A body that grew.** A `deepEquals` on a `POST /v2/translations` body breaks the moment the client adds a field, and the call leaves the imposter. ## Why the mode decides how confusing it is The three Mountebank modes turn the same fault into three different-looking bugs. | Mountebank `mode` | how the fall-through presents | |---|---| | `proxyTransparent` | consistent: every run reaches the real service | | `proxyOnce` | intermittent: real once, then answered from a saved stub | | `proxyAlways` | consistent, and the imposter quietly grows saved stubs | Under `proxyOnce` the symptom is the worst kind: the first run is genuinely live, the reply is saved, and every run afterwards is answered locally from a body nobody wrote or reviewed. The suite looks hermetic again and is now asserting against a snapshot of production data captured on a day nobody remembers. ## Making the fall-through loud 1. **Give the proxy stub a predicate.** A broad proxy does not have to be predicate-free. A `startsWith` on a path prefix that is genuinely third-party means the proxy can only take foreign traffic, and a request meant for a faked glossary path that misses its stub has nowhere to go. 2. **Break the exit while you diagnose.** Point `to` at an address that cannot answer, or remove the proxy stub entirely, and every fall-through turns into a visible failure naming the request that caused it. 3. **Prefer `proxyTransparent` during investigation.** It removes the saved-stub layer, so what you observe on the first run is what happens on every run. 4. **Loosen the specific predicate deliberately.** Key it on the fields the response actually depends on - method, path, the locale parameters - rather than on a whole `query` or `headers` object that will collect fields you did not anticipate. 5. **Watch what the imposter actually received.** The requests the imposter recorded show whether the call arrived at all and in what shape, which separates a matching failure from a client that never reached the imposter in the first place. ## The judgment behind it The deeper point is that selective pass-through spends a safety property to buy convenience. A stub server that answers only what it was told about will fail an unmatched request loudly; the moment you put a broad proxy underneath, every mistake in the specific stubs degrades into a silent, successful, real call. That is an acceptable trade when the proxied portion is genuinely foreign to the test - a metrics sink, an unrelated service - and a bad one when the proxy's reach overlaps the very API you are faking. So the shape to argue for is a narrow proxy, not a broad one: name the traffic that is allowed to escape rather than treating escape as the default. A predicate on the proxy stub costs one line and converts an entire class of invisible failure into an ordinary, diagnosable no-match.
- How would you prove a Mountebank suite is no longer hitting the real glossary service at all?Point the proxy stub's `to` at an address that cannot answer, or drop the proxy stub, and run the suite. A green run proves every request was answered by a specific stub; a failure names exactly which call was escaping. It is a cheap, repeatable check and it belongs in the pipeline, not only in a debugging session.
- Why is a proxy stub with no predicates riskier than one with a startsWith on a path prefix?A predicate-free Mountebank proxy stub takes anything the specific stubs missed, so a broken fixture becomes a successful live call. A `startsWith` on a genuinely third-party prefix limits what may escape: a glossary request that misses its specific stub matches no stub at all and fails visibly, while unrelated traffic still passes through as intended.
saying these in an interview costs you the question
- Assuming an unmatched request always fails visibly
- Treating a green suite as proof nothing left the imposter
- Leaving a proxy stub predicate-free because it is simpler
- Blaming flakiness rather than a saved proxyOnce reply
- Diagnosing with proxyOnce still enabled
- Believing a real upstream reply cannot break a hermetic claim