Reputation: 571
Here is an expectation that utilizes a custom RSpec matcher, yield_variables:
specify { expect{ |p| [3,4,5].my_each(&p) }.to yield_variables [3,4,5] }
my yield_variables matcher's matches? method utilizes a custom class called Probe (Probe is a stripped down version of RSpec's yield probe):
...
def matches? block
ap Probe.probe block
# note i am inspecting what is returned by Probe.probe with ap
end
...
# Probe class is what all my questions are about!
class Probe
attr_accessor :yielded_args
def initialize
self.yielded_args = []
end
def self.probe(block)
probe = new
block.call(probe)
probe.yielded_args
end
def to_proc
Proc.new { |*args| yielded_args << args }
end
end
Now my ap inside matches?
reports this: [3,4,5]
That is what I expect. However, I have no idea how the Probe class works!!
Problem 1) the matches? block
Normally, the argument we pass to matches? is what we expect the subject to return. i.e, I expect [3,4,5]
to be passed into block.
Instead, |p| [3,4,5].my_each(&p)
is passed into block, as a proc. Why is this?
Problem 2) block.call(probe)
I'm a bit shakey on procs so please explain slowly. But basically, here we take a new instance of my Probe class and 'send' the block to it, as an argument. That's how I'd explain it to the best of my abilities, but I might have it totally wrong so please explain slowly.
Problem 3) How is to_proc called?
How on earth is .to_proc called automatically? I believe it's called automatically by block.call(probe). Is to_proc an automatically called method like initialize? Is it automatically called whenever the class is sent to a proc? (Btw, the phrase the class is sent to a proc doesn't even make 100% sense to me - please explain. The block isn't passed into the class as an argument anymore. Or if the block is passed as an argument the block.call syntax feels really weird and backwards)
Problem 4) to_proc
How does to_proc have access to the expectation's subject i.e. |p| [3,4,5].my_each(&p)
? How is Proc.new 's *args automatically populated with every single possible yield argument, even though I've only passed in |p| ? How does Proc.new loop along with my my_each, incrementally placing all my args in an array? How does Proc.new have any knowledge of the subject |p| [3,4,5].my_each(&p)
? How?? Explain please, and thanks.
Upvotes: 3
Views: 726
Reputation: 84114
Block based marchers work a little differently to other matchers. Typically you want to do something that would not be possible if the expression you were interested in was evaluated and the result passed to you.
For example the raises_error
matcher wants to execute the block itself to see that the correct exception is raised and the change
matcher wants to evaluate some other expression before and after to see if it changes in the specified way. This is why you are passed a block rather than the value.
The block you are passing to expect
takes 1 argument and uses this as the block in the call to my_each
, so when your Probe.probe method calls the block it has to pass something as the value. You've phrased as "sending the block to the probe instance" but it is the other way around: the block is called using probe as its argument.
The block executes and calls my_each
. Inside here p
is the instance of Probe
. Prefixing an argument with a & tells ruby that this argument should be used as the method's block (the method being my_each). If the argument is not already a proc ruby calls to_proc
on it. This is how Probe#to_proc is called
Assuming that my_each
behaves in a similar way to normal each
it will call its block (ie the return value of to_proc
) once for each of the values in the array.
Normally your matcher would then compare the return value from Probe.probe
to the actual value.
Upvotes: 2