How would you stub a collaborator to reach the branch that only runs when it fails?
answer
- A working dependency will not fail on request
- Imitate a failure the real one can produce
- Same type, same payload, same wrapping
- Assert the compensation, not the setup
- One failure mode per test
basics
~20 sConfigure the stand-in to produce the failure the real collaborator produces - the same error type and shape - instead of a value. Then assert the observable outcome of the failure branch, not that the stand-in was set to fail.
solid answer
~50 sError branches are the hardest paths to reach with a real collaborator, because you cannot ask a working dependency to fail on cue. A stand-in can: instead of returning a value, it raises or returns the failure. The judgement is in the fidelity. Reproduce the failure the real collaborator can actually produce - the same error type, the same fields, wrapped the same way it would arrive at this seam - because a test that simulates an invented error proves the code handles something that will never occur. Choose failures the unit is genuinely required to survive: an insufficient-points rejection, a rejected debit. Then assert the outcome: the declined result, the compensating entry, the recorded reason. Do not assert that the stand-in was configured. Finally, keep one test per failure mode; a single test that makes everything fail at once tells you nothing about which handler ran.
code
pseudocode · 8 linesledger = stub()
when ledger.debit(member, 1250) then raise InsufficientPointsError(shortfall = 1)
outcome = RedemptionService(ledger, journal).redeem(member, 1250)
assert outcome.status == DECLINED
assert outcome.reason == INSUFFICIENT_POINTS
assert journal.lastEntry.shortfall == 1go deeper
Know that a stand-in can be told to fail instead of returning a value, and that this is the normal way to run the code that only executes when a dependency rejects a request.
Explain how to make the stand-in produce the same error type and payload the real collaborator produces, and why the assertion belongs on what the unit did about the failure rather than on the failure itself.
Demonstrate that you pick failure modes deliberately - rejection, race, partial completion where one call succeeds and the next fails - and that you know a simulated error proves the handler, not the system.
Own the strategy: decide which failure modes the team is required to cover, how the canned failures stay aligned with the collaborators' real contracts, and where a small set of tests against real collaborators has to backstop the simulated ones.
### Why the error branch needs a stand-in The happy path can be reached with real collaborators. The failure path usually cannot: a working dependency will not fail to order, and the conditions that make it fail - exhausted balances, rejected writes, invalid states - are exactly the ones that are hard to arrange on demand. So the error handler ships untested more often than any other code in the system, and it is the code that runs when things are already going badly. Stubbing solves this directly. Instead of configuring the stand-in to return a value, configure it to produce the failure: raise the error, or return the rejection result, whichever shape the real collaborator uses at that seam. ### Fidelity is the whole game The test is only as good as the failure it imitates. Three questions decide that: **Is this failure real?** Simulate something the collaborator can actually produce. A debit against a loyalty-points ledger can be rejected for insufficient points, for a closed account, for a duplicate idempotency key. Inventing a failure the ledger never emits produces a handler nobody will ever execute, and worse, it gives the team confidence that the error path is covered. **Is it the right shape?** If the real rejection arrives carrying the shortfall - a request for 1,250 points against a balance of 1,249 - then the simulated one should carry it too, because the handler probably reads it to build the message the member sees. A bare failure with an empty payload lets a handler pass that would break on the real thing. **Is it wrapped the way it arrives here?** Layers often translate errors on the way up. Simulate the failure as it reaches *this* unit, not as it left its origin, or the test proves the handling of an error that never gets to this code in that form. ### Choose the failure modes deliberately A senior answer names which failures matter rather than reaching for one generic error. For a redemption flow the interesting set is small and specific: the balance check rejects, the debit is refused after the check passed (a race), and the debit succeeds but the confirmation write fails, leaving points deducted with nothing recorded. That last one is where the real bugs are, and it is only reachable by making one collaborator succeed and the next one fail. Related, and a classic: an off-by-one on the boundary of the failure condition. If redeeming is allowed only while the remaining balance stays at or above zero, the three cases worth stubbing are a redemption of 1,249, 1,250 and 1,251 against a 1,250-point balance. The middle one is the case a strict comparison gets wrong, and it is the one a single generic failure test never reaches. ### Assert the outcome, never the setup After the stand-in fails, the test must say what the unit should have done about it. Concretely: the call returned a declined outcome rather than propagating a raw error; a compensating entry was written; the reason recorded matches the failure; the member's balance is unchanged. Those are observable facts about the unit. The failure mode to avoid is a test that stubs the error, catches it, and asserts only that an error occurred - which is a restatement of the setup. If the unit is supposed to swallow the failure and return a default, assert the default. If it is supposed to translate the failure, assert the translated type and its contents. ### One failure per test A test that makes every collaborator fail at once cannot tell you which handler ran, and it passes for as long as *any* of them produces the observed outcome. Keep one failure mode per test, name the test after it, and let the set of tests read as a list of the things this unit survives. ### The limits of a simulated failure Be honest about what this technique does not prove. It shows the handler behaves correctly *given* that error. It does not show the collaborator produces that error under the conditions you assume, and a stand-in whose canned failures no longer match what the real thing emits will keep passing while production breaks. That is why simulated-failure tests are paired with a small number of tests that exercise the real collaborator, and why the canned failures deserve a review whenever the collaborator's contract changes.
- Why is simulating an error the real collaborator never emits worse than having no test at all?Because it converts absence of coverage into false confidence. The handler passes for a case that cannot occur, the real failure mode stays unexercised, and the green test discourages anyone from looking. A missing test at least reads as missing on a coverage report or in review.
- How do you test the case where one collaborator succeeds and the next one fails?Configure them independently: let the debit succeed and make the confirmation write fail. That reaches the partial-completion path - points deducted with nothing recorded - which is where the damaging bugs live and which a single all-collaborators-fail test can never isolate.
- What should the test assert if the unit is supposed to swallow the failure and return a default?Assert the default value the caller receives, plus any side effect the swallow is meant to leave behind, such as a recorded reason or an incremented counter. Asserting only that no error escaped would pass equally for a unit that silently did nothing at all.
- Does a simulated failure prove the system handles that failure in production?No. It proves the handler is correct given that error shape. It says nothing about whether the collaborator really produces that error under those conditions, so the canned failures need reviewing when the collaborator's contract changes, and a few tests against the real collaborator remain necessary.
saying these in an interview costs you the question
- Simulates a generic failure the collaborator never produces
- Asserts only that an error occurred, restating the setup
- Makes every collaborator fail in one test
- Ignores the failure's payload the handler actually reads
- Simulates the error as it leaves its origin, not as it arrives here
- Claims a simulated failure proves production resilience