JayTarka
JayTarka

Reputation: 571

Explain the usage of procs in an rspec context

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

Answers (1)

Frederick Cheung
Frederick Cheung

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

Related Questions