Invoice#total, oldest call first. Each argument is an
object built earlier on the path.
LineItem and Integer, never which line item or how many.
The problem
If you’d rather test with real collaborators than doubles, the cost is setup. To call
Invoice#total for real you need an Invoice, which needs line items and a rate table, which need
an account. Nothing tells you which of those are required or what order to build them in, so you
read the code, guess, and debug the guess.
That setup tax is the main reason people reach for doubles when they’d rather not.
The fix
Your agent asks while writing an integration spec:
how_to_reach("Invoice#total")
→ {
path: [
"Account.new(plan: Plan)",
"RateTable.for(account: Account)",
"Invoice.new(account: Account, rate_table: RateTable)",
"Invoice#add_line(item: LineItem, quantity: Integer)",
"Invoice#total"
],
seen_in: "spec/requests/billing_spec.rb:31"
}
It turns that into setup that uses real objects throughout:
account = Account.new(plan: Plan.new(:standard))
rate_table = RateTable.for(account:)
invoice = Invoice.new(account:, rate_table:)
invoice.add_line(item: LineItem.new(sku: "A1"), quantity: 2)
expect(invoice.total).to eq(Money.new(2_400, "GBP"))
The classes and call order come from the recording. The values are yours: the recording keeps class names, never your data.
How it works
The recording keeps object identity. how_to_reach finds a recorded call to the target method, takes
its receiver, and walks backwards through the call tree to where that object was built and every
call made on it before the target. It does the same for each argument that was passed along the
way. Of all the recorded ways to reach the method, it picks the shortest, and names the spec
example or run it came from.
Limits
- Classes, not values. It knows
add_linewas called with aLineItemand anInteger. Which line item and how many are up to you. - Only paths that ran. If the state a method needs was only ever reached through code you didn’t record, there’s no path to offer.
- Shortest isn’t always simplest. The shortest recorded path might go through a factory or a service object. You can ask for the paths that avoid a given class.
Related tools
testing_pyramid
The boundaries your pyramid misses. Seams that are only ever crossed through a double, never for real, mapped onto your testing pyramid. Record production too and they're ranked by traffic.
See more →verify_mock
Catch a lying mock. A stub that passes green but returns something the real collaborator never would. Mutation testing can't see it. The recording can.
See more →observed_shape
The observed contract. The messages your code actually sends to a value, read from real runs. The interface a call depends on, whether or not anyone declared it.
See more →