Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RSpec thinks that block does not receive "call" message?

Tags:

ruby

rspec

I would like to use RSpec to ensure that my enumerable class is compatible with Ruby's visitor pattern:

# foo.rb
class Foo
  def initialize(enum)
    @enum = enum
  end
  include Enumerable
  def each(&block)
    @enum.each(&block)
  end
end

Here is my rspec file:

# spec/foo_spec.rb
require 'rspec' 
require './foo.rb' 

describe Foo do 
  let(:items) { [1, 2, 3] } 
  describe '#each' do 
    it 'calls the given block each time' do 
      block = proc { |x| x }
      block.should_receive(:call).exactly(items.size).times 
      Foo.new(items).each(&block) 
    end 
  end 
end

But surprisingly, my examples fail when run (with rspec v2.14.5):

# $ bundle exec rspec

Failures:

  1) Foo#each calls the given block each time
     Failure/Error: block.should_receive(:call).exactly(items.size).times
       (#<Proc:0x007fbabbdf3f90@/private/tmp/rspec-mystery/spec/foo_spec.rb:8>).call(any args)
           expected: 3 times with any arguments
           received: 0 times with any arguments
     # ./spec/foo_spec.rb:12:in `block (3 levels) in <top (required)>'

Finished in 0.00082 seconds
1 example, 1 failure

Failed examples:

rspec ./spec/foo_spec.rb:11 # Foo#each calls the given block each time

Even more surprising, the class itself behaves exactly as I expect when used via ruby/irb:

# $ irb -r ./foo.rb

1.9.3-p125 :002 > f = Foo.new [1, 2, 3]
 => #<Foo:0x007ffda4059f70 @enum=[1, 2, 3]> 
1.9.3-p125 :003 > f.each
 => #<Enumerator: [1, 2, 3]:each> 
1.9.3-p125 :004 > block = proc { |x| puts "OK: #{x}" }
 => #<Proc:0x007ffda483fcd0@(irb):4> 
1.9.3-p125 :005 > f.each &block
OK: 1
OK: 2
OK: 3
 => [1, 2, 3] 

Why doesn't RSpec notice that the "block" does in fact receive the "call" message three times?

like image 265
maerics Avatar asked Oct 27 '25 18:10

maerics


1 Answers

Why doesn't RSpec notice that the "block" does in fact receive the "call" message three times?

Because, AFAICT, on MRI, it doesn't.

#each isn't provided by Enumerable, only by the classes that implement it, and in your test, you're using an Array.

Here's the source code (in C) from Array#each:

VALUE rb_ary_each(VALUE array)
{
    long i;
    volatile VALUE ary = array;

    RETURN_SIZED_ENUMERATOR(ary, 0, 0, rb_ary_length);
    for (i=0; i<RARRAY_LEN(ary); i++) {
        rb_yield(RARRAY_PTR(ary)[i]);
    }
    return ary;
}

From this, it looks like Array#each yields to the block rather than calling it explicitly.

UPDATE:

Your code & test fails on Rubinius & JRuby as well, so it looks like their standard libraries don't use call here either. As @mechanicalfish points out, you really only need to test that the iterator goes over the collection the correct number of times.

like image 184
andrewdotnich Avatar answered Oct 30 '25 09:10

andrewdotnich



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!