WestonGanger WestonGanger - 3 months ago 6
Ruby Question

Access variable from the block's scope inside a method call in Ruby

I am trying to create a minitest assertion that is similiar to Rails

however it checks for any differences not just a numeric one.

Here is my current implementation:

Minitest::Assertions.module_eval do

def assert_changed(expression, &block)
unless expression.respond_to?(:call)
expression = lambda{ eval(expression, block.binding) }
end
old = expression.call
block.call
refute_equal old, expression.call
end

end


Now in my test suite I call it by doing the following:

# this is working
username = 'Jim'
assert_changed lambda{ username } do
username = 'Bob'
end

# this is not working
username = 'Jim'
assert_changed 'username' do
username = 'Bob'
end


The first call to
assert_changed
using a lambda (or a proc) works perfectly fine. The second call using a string with the variable name is not working.

When it hits this line:
expression = lambda{ eval(expression, block.binding) }

I keep getting the error
TypeError: no implicit conversion of Proc into String
. Can someone please explain to me how to get this to work?

Note: I got the
eval(expression, block.binding)
idea from the Rails
assert_difference
method.

Answer

When it hits this line: expression = lambda{ eval(expression, block.binding) } I keep getting the error TypeError: no implicit conversion of Proc into String.

I'm reasonably sure that you do not get that error when hitting that line but rather when hitting this one:

old = expression.call

So, the exception is not triggered by the assignment, but rather later, when the Proc is called and the eval gets executed.

At this point, it will call the Proc stored in expression. The Proc stored in expression looks like this:

eval(expression, block.binding)

The first argument to Kernel#eval must be something that is implicitly convertible to String (i.e. something that responds to to_str), but it is a Proc. Ergo, you get a TypeError.

The easiest way to make this work would probably be to just rename the variable:

new_expression = expression
unless expression.respond_to?(:call)
  new_expression = lambda{ eval(expression, block.binding) }
end
old = new_expression.call
block.call
refute_equal old, new_expression.call

Personally, I would have probably written it more like this:

module Minitest::Assertions
  def assert_changed(expression, &block)
    new_expression = if expression.respond_to?(:call)
      expression
    else
      -> { block.binding.eval(expression) }
    end

    old = new_expression.()
    block.()

    refute_equal old, new_expression.()
  end
end

Without the (useless) Module#module_eval, and using the call operator syntax and -> stabby lambda literal syntax.

Comments