Skip to content

Test doubles and boundaries

Choose the collaborator to control, then preserve the behavior being examined.

Predict first

Before you read: predict

A streaming test supplies this partner substitute: { submit: () => ({ accepted: true }) }. The app returns a draft, and the test checks that draft's contents.

Which double role describes the supplied object?

Pick one

The 30-second version

A stub supplies programmed replies; a fake runs a smaller working implementation; a spy records calls; a mock verifies an interaction contract. Identify what the test owns and which dependency needs control. Clock readings and scheduled callbacks are separate observations. Inspect each double's behavior and the owned result. Fowler · Vitest

Choose the collaborator to control, then preserve the behavior being examined.

How it works

Step 1 of 7

Supply an answer. A stub provides a programmed reply to the call made by the system. Fowler

Analogy: An electronics calibration bench

An engineer calibrates a pulse counter. A fixed signal source emits the programmed level: it stands for a stub. A small working oscillator generates a useful range of pulses while lacking the production instrument's stability: it stands for a fake. A recording channel logs the incoming edges: it stands for a spy. A verification checklist names the pulse sequence the counter must request from the source: it stands for a mock.

The engineer keeps the counter's counting circuit connected. Replacing both the source and that circuit with predetermined displays would conceal a counting fault. The controlled source makes repeatable inputs available; the counter's own output remains something to inspect. Logging a pulse and supplying a pulse are different jobs, even when one laboratory box offers both functions.

Calibration bench Software correspondence
Fixed signal source Programmed reply
Small working oscillator Useful implementation shortcut
Recording channel Call history
Required pulse-sequence checklist Interaction expectation
Counter circuit under examination Owned behavior
Adjustable source settings Controlled collaborator

The analogy breaks down at software protocols. A physical signal cannot represent an object payload, a database transaction, or an asynchronous callback queue. Nor does a pulse recorder necessarily wrap and delegate another instrument. The Vitest API's delegation and replacement behavior must be learned from its actual contract, rather than inferred from this picture.

Examples, run for real

Candidate app reminder batch (EXH-1870). The executed local service reserves exam-open, seat-ready and room-changed. A gateway accepts the first two and rejects the third. The batch retains accepted reservations and removes the rejected reservation. An ordinary spyOn observes the gateway, and Testing Library reads the resulting status.

Teaching exam: attempts 3; accepted 2; rejected 1; retained exam-open, seat-ready; status 2 reminders retained

Media-streaming reminders. A second executed batch uses three episode subscriptions. Its working local gateway records the received reminder IDs and produces two acceptances followed by a rejection. The test checks the retained IDs, reply counts and visible status together. The recorder delegates to that local implementation; this example installs no replacement body.

Teaching streaming: attempts 3; accepted 2; rejected 1; retained episode-42, episode-43; status 2 reminders retained

The retained IDs explain the product outcome; the interaction record explains which requests produced it. Neither observation demonstrates a production partner contract. These fixtures illustrate one collaborator decision, not an overall test mix. test-shape-pyramid-trophy owns that broader strategy; test-react-testing-library owns query selection and asynchronous UI waiting.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 6

A caption app uses CaptionCodec to build a payload, DraftStore to retain pending work, and PartnerClient to obtain a review decision. The codec and store ship in the app repository; the client's endpoint belongs to a playback partner.

Which setup fits a repeatable local submission test?

Pick one

Question 2 of 6

A caption cache uses save({questionId, answer}) and read(questionId). The harness saves q-17:A, q-17:B, q-22:C, then reads q-17, q-22, q-17.

Which substitute implements the cache's update-and-lookup contract?

Pick one

Question 3 of 6

A gateway's notify(message) appends the message to delivered and returns 'sent'. The test applies vi.spyOn(gateway, 'notify').mockImplementation(message => 'queued:' + message), invokes gateway.notify('episode-9') once, and reads [recordedCalls, delivered.length, returned].

Which tuple is observed?

Pick one

Question 4 of 6

Before exercising a subscription checkout, its substitute is configured with an ordered reserve() then commit() agreement. Verification compares the received sequence with that agreement.

Which role describes this substitute?

Pick one

Question 5 of 6

A subtitle-licensing coordinator reserves license-83, calls the licensing adapter, and handles the returned decision. The app owns DraftStore; the adapter talks to the rights service. The local response fixture is {accepted:false}. The check exercises cleanup through the coordinator and inspects the remaining records.

Which setup makes the collaborator boundary suitable?

Pick one

Question 6 of 6

A subtitle lease reads Date.now() >= 1500 as expired and otherwise open. Its callback counter starts at 0. The test runs vi.useFakeTimers(); vi.setSystemTime(1000), schedules the counter increment with setTimeout(record, 400), calls vi.setSystemTime(1600), takes a snapshot, calls vi.advanceTimersByTime(400), then takes another snapshot.

Which pair of [leaseState, callbackCount] snapshots is observed?

Pick one

Go deeper

Pro tip

Inspect the configured implementation as well as the call history. A method wrapper can be observing, replacing or verifying behavior in one test. Name what its configuration controls and which product result remains meaningful. Vitest mock functions

Checked against the tools and docs as of 2026-09-30.