def unlock_next
curriculum
.unlocked_for(student)
.select(&:unlocked?)
.map(&:id)
end #unlocked?#id · across 214 deploys of your app Book appears in your suite; the other two were found in pooled production traffic.
The problem
verify_mock has just told you a stub lies. Enrollment#unlock_next calls #unlocked? and #id on
each thing Curriculum#unlocked_for returns, and your double hands back Symbols:
curriculum = instance_double(Curriculum)
allow(curriculum).to receive(:unlocked_for).and_return([:geometry])
You know the shape now: something that answers #unlocked? and #id. What you don’t know is which
real class to use instead. Book is the obvious one, but building a Book needs a Publisher, a
Series and a price. Somewhere in the app there might be a lighter class that plays the same role.
Nobody declared that role, so nothing will tell you.
Grep can’t. It finds classes that define unlocked?, including the ones whose unlocked? takes an
argument, or that nothing has ever passed to Enrollment. The recording on your laptop can’t either:
it only knows the classes your suite happened to run, and a class that only shows up in production
traffic isn’t in it.
The fix
Ask for implementers of the message set the call site needs:
$ ra find_implementers '#unlocked?, #id'
real types responding to {#unlocked?, #id} pooled from 214 deploys
Book 94,210 prod runs built in 3 calls: Publisher, Series, Book.new
SampleBook 8,904 prod runs built in 1 call: SampleBook.new(title:)
ArchivedBook 611 prod runs built in 2 calls: Book, ArchivedBook.new(book)
→ swap one in, or generate a verified fake from the contract
SampleBook answers both messages, it’s built in one call, and it has been passed through real
code nearly nine thousand times. Use it:
book = SampleBook.new(title: "Geometry")
curriculum = instance_double(Curriculum, unlocked_for: [book])
expect(Enrollment.new(curriculum:).unlock_next).to eq([book])
Or, if you’d rather keep a double, ask your agent for a fake built from the recorded contract. It
answers exactly the messages real callers sent, with the return classes they got back, so
verify_mock passes it by construction.
How it works
- Read the required set. From a call site, or from a lying stub, the recording gives the messages
the code sends to a value: here
#unlocked?and#id. - Search the pooled graph. The hosted graph holds every recorded receiver of every message, from every deploy. It finds each class whose instances were observed receiving all the messages in the set, with argument classes that match.
- Rank by evidence. Classes seen more often, in more places, rank higher. Each comes with the
shortest recorded path that constructed one, the same recipe
how_to_reachgives you locally.
The pooling is the point. One laptop’s recording sees the classes one suite happened to run. A hundred deploys’ worth of production traffic sees the classes your app actually passes around, including the ones no spec mentions.
Limits
- Observed, not possible. A class that could answer both messages but was never seen doing so isn’t listed. That’s usually what you want: a receiver the code has really used.
- Messages, not meaning.
#unlocked?on aDooranswers the same message as#unlocked?on aBook. Results are ranked by where they were seen, so a class from a different part of the app sorts lower, but it’s worth a glance. - Needs production recording. It runs on the hosted graph, fed by the deploys you record.
Related tools
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 →who_actually_answered
Who actually answered. At a duck-typed call site, the concrete classes that received the message at runtime, and how often. Go to implementation, from real runs.
See more →