def summary_line(book)
"#{book.title} · #{book.weeks} weeks"
end class Book
def title = …
def weeks = …
def author = …
def curriculum = …
def price = …
end #title#weeks · 22 other public methods never sent · 9 examples · 2,406 prod runs Book is, summary_line only ever asks it for a title and a length.
The problem
A parameter name is a guess about what a method needs. This one says book:
class Report
def summary_line(book)
"#{book.title} · #{book.weeks} weeks"
end
end
So its spec builds a book, and a real Book needs an author, a curriculum and a price before it will
save:
it "prints the title and length" do
book = create(:book, :with_author, :with_curriculum, weeks: 12, title: "Geometry")
expect(Report.new.summary_line(book)).to eq("Geometry · 12 weeks")
end
The spec is slow, it breaks whenever Book’s validations change, and none of that setup has anything
to do with a summary line. The method sends two messages to its argument. Nothing in the code says so
except the body, and the body gets longer.
The same question comes up when you want to reuse the method. Can summary_line take a Course? Only
if you know what it actually asks of what it’s given.
The fix
Ask what the argument receives:
$ ra what_it_receives Report#summary_line
book (arg 1)
classes Book 41 calls · 3 call sites
receives #title #weeks
never #author #curriculum #price #chapters … (19 more public methods on Book)
· from 9 spec examples and 2,406 production runs
summary_line depends on something that answers #title and #weeks. That’s the whole contract, so
that’s all the spec has to supply:
it "prints the title and length" do
book = instance_double(Book, title: "Geometry", weeks: 12)
expect(Report.new.summary_line(book)).to eq("Geometry · 12 weeks")
end
And the reuse question has an answer: anything with #title and #weeks will do, so a Course that
has both can be passed straight in. Rename the parameter to say so, if you like.
Where it shows up
- Your editor (LSP). Hover an argument and see its classes and the messages it receives.
- Your agent (MCP). Before writing a double or a factory call, the agent asks what the method needs and builds only that.
- The command line.
ra what_it_receives Report#summary_line, per argument, and per call site with--by-callerwhen different callers pass different things.
How it works
- Argument classes. Every recorded call to
Report#summary_linecarries the class of each argument it was passed. That gives theclassesline. - Argument identity. The recording also keeps each argument’s object identity. Inside one call tree, an object is live and its id can’t be reused, so the argument can be followed to every later call where it’s the receiver.
- Collect the messages. Those calls are the messages sent to the argument while the method ran, including by anything it passed the argument on to.
- Aggregate. The message sets are merged across every call tree, and kept per call site so you can see when one caller’s argument is asked for more than another’s.
Limits
- App methods only, for now. Messages to your own classes are recorded. A message to a core type,
like
#to_son aString, isn’t yet, so an argument that only ever gets#to_sshows no messages. - Only what ran. A branch that sends
#authorbut never ran in any recording won’t show up. Readreceivesas “was asked for”, not “can only be asked for”. - Shapes, not values. It knows
bookwas aBookthat answered#weeks. It doesn’t know it was 12.
Related tools
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 →how_to_reach
Reach any method. The collaborators and call path that get an object into the state a method needs, taken from a real run. Setup for tests that use real objects instead of doubles.
See more →