small medium large xlarge

Generic-user-small
17 Apr 2017, 16:20
Bernard Kaiflin (14 posts)

The book (section When You Need More Flexibility) and other sources say :

If you’re not quite sure how to configure a double to do what you need, you can supply a block containing whatever custom behavior you need. Simply pass the block to the last method in the receive expression. … RSpec will run this block and … return a value …

From https://github.com/rspec/rspec-mocks :

expect(double).to receive(:msg) { value }

When the double receives the msg message, it evaluates the block and returns the result.

I have experienced that it doesn’t work when the method name ends with = or is defined by attr_accessor. Explanations found :

When calling an attribute setter in Ruby, the method returns the new value of the attribute (which is the passed parameter). [http://blog.silvabox.com/what-to-return-from-a-ruby-method/]

An assignment-like method call always returns its right value rather than the return value from the method. [https://bugs.ruby-lang.org/issues/7773]

The following somewhat contrived code illustrates that. Instead of returning the block value, this assignment returns the value of the argument :

    allow(class_a).to receive(:setter=) { | arg | ... block value }
    result = class_a.setter=('xyz') # => 'xyz'

File lib/app.rb :

class A
    def getter(parm)
    end

    def setter=(parm)
        puts "        === in setter=, parm=#{parm.inspect}"
        B.new
    end
end

class B
    attr_accessor :some_value
end

File spec/app_spec.rb :

require 'app'

RSpec.describe A, 'stores Bs' do
  context 'mock' do
    let(:class_a) { instance_double(A) }

    def helper_create_b(arg, meth)
        puts "        ... #{meth} called with #{arg}"
        obj = B.new
        obj.some_value = arg
        obj
    end

    it 'a mocked getter/ordinary method returns the value of the block' do
        allow(class_a).to receive(:getter) { | arg | helper_create_b(arg, :getter) }

        result = class_a.getter('abc')
        puts "        ... result=#{result.inspect}"
        expect(result.instance_of?(B)).to be true
    end

    it 'a mocked setter returns the value of the block (false !)' do
        allow(class_a).to receive(:setter=) { | arg | helper_create_b(arg, :setter=) }

        result = class_a.setter=('def')
        puts "        ... result=#{result.inspect}"
        expect(result.instance_of?(B)).to be true
    end
  end

  context 'no mock' do
    it 'a setter does not return the value of the method' do
        result = A.new.setter=('ghi')
        puts "        ... result=#{result.inspect}"
        expect(result.instance_of?(B)).to be false
    end

    it 'a setter returns the value of the argument' do
        result = A.new.setter=('jkl')
        puts "        ... result=#{result.inspect}"
        expect(result).to eq('jkl')
    end

    it 'an attr_accessor returns the argument' do
        result = B.new.some_value=('mno')
        puts "        ... result=#{result.inspect}"
        expect(result).to eq('mno')
    end
  end
end

Execution :


$ rspec spec/app_spec.rb
A stores Bs
  mock
        ... getter called with abc
        ... result=#<B:0x007f949a98ace8 @some_value="abc">
    a mocked getter/ordinary method returns the value of the block
        ... setter= called with def
        ... result="def"
    a mocked setter returns the value of the block (false !) (FAILED - 1)
  no mock
        === in setter=, parm="ghi"
        ... result="ghi"
    a setter does not return the value of the method
        === in setter=, parm="jkl"
        ... result="jkl"
    a setter returns the value of the argument
        ... result="mno"
    an attr_accessor returns the argument

Failures:

  1) A stores Bs mock a mocked setter returns the value of the block (false !)
     Failure/Error: expect(result.instance_of?(B)).to be true
     
       expected true
            got false
     # ./spec/app_spec.rb:62:in `block (3 levels) in <top (required)>'
Myron-profile-img_pragsmall
17 Apr 2017, 16:45
Myron Marston (7 posts)

That’s a very astute observation, Bernard! You are right. Ruby does not allow us to determine the return value when the method name ends in a =. I think Ruby enforces this for consistency in chained assignment statements. For example, you can do this:

x = y 7

This sets both x and y to 7. Likewise, you can do this:

a.x = b.y = 7

But that only works if b.y returns 7.

Sorry if this behavior surprised you, but there’s nothing RSpec can do about it.

You must be logged in to comment