skip to content

What is stubbing a collaborator's method in a unit test, and why do it?

level: juniorimportance: must knowfreq 72%

answer

  1. The test decides what the dependency says
  2. Control the input, then assert the output
  3. Canned answer, chosen in advance
  4. Reach the 2,500-point boundary in one line

basics

~10 s

Stubbing replaces a collaborator's method with a canned answer the test chose in advance. The unit under test then runs down a known path, without depending on the real dependency's data, speed or availability.

solid answer

~50 s

A stub is a stand-in installed in place of a real collaborator whose job is to hand back a predetermined answer. The test says: when the unit asks for the balance, report 2,500 points. That gives the test control over the inputs the unit receives sideways - through the things it calls - rather than only through its own parameters. The value is that a specific path becomes cheap to reach: instead of building real state until the numbers happen to line up, the test states the number directly and asserts the resulting behaviour. Stubbing also removes slowness and nondeterminism from the test, but steering is the main reason. Crucially, the canned answer is only an input. The assertion still belongs on what the unit produced or changed; if the test asserts nothing but the setup, it has stubbed away the behaviour it meant to check.

code

pseudocode · 6 lines
pseudocode
ledger = stub()
when ledger.redeemableBalanceFor(member) then return 2500

tier = TierCalculator(ledger).tierFor(member)

assert tier == GOLD

go deeper

for a junior

Be ready to define a stub in one sentence - a stand-in that returns an answer the test chose - and to say why: the test needs a specific path to be cheap and repeatable to reach.

for a middle

Explain the mechanics: which seam the stand-in is installed at, that the canned answer is an input rather than a verdict, and that the assertion still belongs on what the unit returned or changed.

for a senior

Show judgement about what you pin and how. Choose boundary-meaningful values, keep the stubbed surface narrow so refactoring does not break the test, and make sure canned answers mirror shapes the real collaborator can actually produce.

for a principal

Own the tradeoff across a suite: heavy stubbing buys fast, targeted tests but moves risk to the seams nobody exercised, so argue for where the team pays that back with tests that use real collaborators.

### What a stub is A **stub** is a stand-in that a test installs in place of a real collaborator, and whose only job is to hand back an answer the test decided in advance. Nothing about that answer is computed by the real dependency - it is canned. The point is **control**: while the unit under test runs, every value reaching it sideways (not through its own parameters, but through the collaborators it calls) is pinned to something the test author chose. This is why stubbing is described as *state-based* control of dependencies. The test controls the state the unit reads, runs the unit, and then asserts on the state or value the unit produced. ### Why a test wants that control Real collaborators are awkward in four recurring ways. They can be **slow** - a call that crosses a process, a network or a disk. They can be **nondeterministic** - a clock, a random source, a queue whose contents depend on what ran before. They can be **unavailable** - a partner endpoint nobody wants a build to touch. And, most relevant to stubbing, they can be **hard to steer**: to make a real collaborator report exactly the state that triggers the branch you care about, you may have to construct an elaborate world first. ### A worked example Take a loyalty-points ledger. A tier calculator asks the ledger for a member's redeemable balance and returns a tier: Silver below 2,500 points, Gold at 2,500 and above. You suspect an off-by-one at that boundary - the rule may be written with a strict greater-than where it needs greater-or-equal, so a member sitting exactly on 2,500 is quietly left at Silver. Without stubbing, provoking balances of exactly 2,499, 2,500 and 2,501 means accruing and expiring real ledger entries until the arithmetic lands on those totals. On a 4-person team that setup was 17 lines long, longer than the assertion it supported, and it broke every time the accrual rules moved - the test failed for a reason that had nothing to do with tiering. With a stub, each case is one sentence: the ledger reports 2,499, expect Silver; reports 2,500, expect Gold; reports 2,501, expect Gold. The boundary is now stated in the test, in the same units the business rule is written in, and the middle case fails loudly while the strict comparison remains. ### The canned answer is an input, not a verdict The **oracle** - the thing that decides the behaviour is wrong - is still the assertion on what the unit returned, wrote or published. A test that stubs the ledger to report 2,500 and then checks nothing but the setup has proved only that its own scaffolding works. This is the single most common way a stubbed test becomes worthless: the setup grows, the assertion shrinks, and eventually the test cannot fail for a real reason. ### Three shapes of canned answer 1. **A fixed value on every call.** The common case, and the right default. 2. **A different value per call.** Used when the unit is expected to call more than once and react to a change between calls. 3. **A failure instead of a value.** The only reliable way to reach an error branch on demand, because a real dependency will not fail to order. ### Costs and cautions A stub encodes your belief about how the collaborator behaves; if that belief is wrong, the test agrees with you rather than with reality, so the canned answers should mirror shapes the real thing can actually produce. Stubbing a method the unit does not really need also freezes a piece of the implementation into the test: every stubbed method is one more detail the test now knows about, and one more thing that has to change when the unit is refactored. Prefer stubbing at the narrow point the unit actually reads from, and prefer values that are meaningful for the rule under test - a boundary figure like 2,500 teaches a reader more than a placeholder like 1. ### Terminology care In an interview, say plainly that a stub **supplies answers**. Whether a test also asserts *how* a collaborator was called is a separate decision with its own name and its own tradeoffs; running the two ideas together is what makes an otherwise fine answer sound vague. Likewise, a stub is not the same thing as a lightweight working implementation of the collaborator - the stub does not compute, it recites.

  • If a stub supplies the value, what is left for the test to assert?
    Everything the unit produced from that value: the returned tier, the record it wrote, the event it published, the branch it took as visible in observable state. The stub is setup, not evidence. A test whose assertions only restate the stubbed value proves nothing about the unit.
  • Why prefer a meaningful number like 2,500 over a placeholder like 1 in a stubbed answer?
    Because the number is documentation. A boundary figure tells the next reader which rule the case is pinned to and makes an off-by-one visible when the case fails. A placeholder value sits far from every threshold, so the test passes for reasons nobody can name and misses the boundary entirely.
  • Can a collaborator with no return value be stubbed?
    There is nothing to stub in the sense of a canned answer - a method that returns nothing supplies no input to steer. What a test can do is let it do nothing safely, or make it fail so the error branch is reachable. Checking that it was called is a different concern with a different technique.

A stub is a cue card handed to an actor: when asked, they read out exactly the line the director chose, so the rest of the scene can be rehearsed without waiting for the real conversation to happen.

saying these in an interview costs you the question

  • Says a stub records the calls made to it
  • Treats a stub as a fast working implementation of the collaborator
  • Asserts only that the stub was configured, never the outcome
  • Stubs a pure value object with no side effects
  • Uses placeholder values that sit nowhere near the rule's boundary
  • Claims stubbing exists mainly to make tests run faster

context