Before · invoice_spec.rb:4
def total
rate_table = RateTable.for(account:)
…
end After · rate table passed in
The problem
This lives in spec/models, so everyone calls it a unit spec:
RSpec.describe Invoice do
it "adds VAT to the total" do
invoice = Invoice.new(account:)
invoice.add_line(item: LineItem.new(sku: "A1"), quantity: 2)
expect(invoice.total).to eq(Money.new(2_880, "GBP"))
end
end
It takes 400ms. It fails when someone changes the tax rules for Ireland, and again when the currency
cache is refactored. Neither change touched Invoice.
Nothing in the spec says why. Inside, Invoice#total builds its own RateTable, which loads the
account’s plan, the tax rules, a currency converter and a cache. The spec is exercising all of them,
and the only way to find out is to read every method on the path.
The fix
Ask how far each example reached:
$ ra how_far_it_reached spec/models
classes depth example
31 12 spec/models/invoice_spec.rb:4 adds VAT to the total
3 2 spec/models/line_item_spec.rb:6 prices by sku
2 1 spec/models/account_spec.rb:9 knows its plan
· invoice_spec.rb:4 reached TaxRules, CurrencyConverter, RateCache …
Thirty-one classes, twelve calls deep. That’s an integration test in a unit spec’s directory, and the
list of classes shows where the reach comes from: Invoice creates its RateTable itself.
Pass the rate table in, and the unit spec can decide what it’s testing:
class Invoice
def initialize(account:, rate_table: RateTable.for(account:))
@account = account
@rate_table = rate_table
end
end
rate_table = instance_double(RateTable, vat_for: 0.2)
invoice = Invoice.new(account:, rate_table:)
Rerun it and the example reaches three classes. verify_mock checks that double against the real
RateTable in its own spec, and the thirty-one-class path moves to an integration spec that says so.
How it works
Every call tree in the recording is stamped with the spec example that started it. For each example,
how_far_it_reached walks its trees and counts two things:
- Classes: the distinct receiver classes of every recorded call in the tree.
- Depth: the longest chain of calls from the example to a leaf.
Both come straight off the call edges, so the answer is what actually ran, including anything reached
through send, callbacks or a collaborator building its own collaborators.
Limits
- A number, not a verdict. Some units legitimately compose a handful of value objects. It reports the reach and leaves the threshold to you.
- App code only. The recorder wraps methods your app defines, so
String,Hashand library internals don’t count towards the class total. - One run’s worth. An example that takes a different path on a different day reports the path it took when it was recorded.
Related tools
specs_that_reach
Run only the specs that matter. The spec examples whose recorded runs reach a method, so a change reruns those and nothing else. Keeps a red-green loop in seconds.
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 →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 →