For example, the following code will print a warning.
Expectations do
expect 1 do
Object.expects(:something).returns 1
Object.something
end
end
# >> Expectations allows you to to create multiple mock expectations, but suggests that you write another test instead.
# >> expects method called from /Users/jay/example.rb:6
# >>
# >> Expectations .
# >> Finished in 0.001 seconds
# >>
# >> Success: 1 fulfilled
Usually, I'd use one of the various Alternatives for Redefining Methods, but redefining the Object#expects method had two additional constraints that complicated matters.
- There's no (reasonable) way to extend all instances with a new module since the
expects
method is defined on Object. - I didn't want to unconditionally redefine the expects method. Within the framework I call Object#expects, and I don't want those calls to cause invalid warnings. I need a solution that prints a warning when you use Object#expects within an expectation block, but does not print a warning if Object#expects is called from anywhere else.
The actual implementation involves several moving parts, so here's a much simpler example. Start with some behavior defined on Object. This behavior will have been defined by another framework so you cannot (easily) alter the original method definition.
"hello"
end
end
Next, you've decided to create a framework that says hello in Spanish also. You still want to be able to return "hello" when English is required, but you want the
say_hello
method to return "hola" when you are expecting Spanish.Below is the output we are looking for.
in_english do
say_hello # => "hello"
end
in_spanish do
say_hello # => "hola"
end
Currently our code returns "hello" both in English and Spanish.
# Framework Object::say_hello
"hello"
end
end
# Your Object class
instance_eval(&block)
end
instance_eval(&block)
end
end
in_english do
say_hello # => "hello"
end
in_spanish do
say_hello # => "hello"
end
We can make the
say_hello
message sent to Object return "hola" by removing the say_hello
method, defining an InSpanish module, and including the InSpanish module.# Framework Object::say_hello
"hello"
end
end
# Your Object class
"hola"
end
end
include InSpanish
remove_method :say_hello
instance_eval(&block)
end
instance_eval(&block)
end
end
in_english do
say_hello # => "hola"
end
in_spanish do
say_hello # => "hola"
end
Now we have Spanish working, but we've lost our English. Remember the actual implementation needed to preserve the original behavior in some circumstances. The
in_english
method is our circumstance where we need to preserve original behavior. This can be done easily enough by Moving the say_hello definition from Object to an InEnglish module.# Framework Object::say_hello
"hello"
end
end
# Your Object class
"hola"
end
end
include InSpanish
expects_method = Object.instance_method(:say_hello)
define_method :say_hello do |*args|
expects_method.bind(self).call(*args)
end
end
include InEnglish
remove_method :say_hello
instance_eval(&block)
end
instance_eval(&block)
end
end
in_english do
say_hello # => "hello"
end
in_spanish do
say_hello # => "hello"
end
Now we have the original behavior of the
say_hello
method, but we've lost our ability to speak Spanish.The final step is to define Object#say_hello in a way that delegates the
say_hello
message to the appropriate module instead of removing the method.# Framework Object::say_hello
"hello"
end
end
# Your Object class
"hola"
end
end
include InSpanish
expects_method = Object.instance_method(:say_hello)
define_method :say_hello do |*args|
expects_method.bind(self).call(*args)
end
end
include InEnglish
(@language || InEnglish).instance_method(:say_hello).bind(self).call
end
@language = InEnglish
instance_eval(&block)
end
@language = InSpanish
instance_eval(&block)
end
end
in_english do
say_hello # => "hello"
end
in_spanish do
say_hello # => "hola"
end
The final change was setting the
@language
instance variable to the module who's say_hello
method definition was required. As you can see from the printed output, our code works as desired.This isn't a technique that you'll use often, but it's a good trick to know when you need it. If you're interested in the actual application you can check out the expectations framework code.
Jay, thanks for the great post. It occurred to me that this is very similar to some of the code used in Neal Ford's Design Patterns in Ruby talk (RubyConf 2008 and others).
ReplyDeleteIn that presentation, he briefly showed off the features of a gem (by your coworkers, oddly enough) called mixology that may be a nice substitute for the code in this article.
References:
blog article
Neal Ford's Design Patterns in Ruby presentation(pdf)