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 fulfilledUsually, 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
expectsmethod 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.
class Object
def say_hello
"hello"
end
endNext, 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
class Object
def say_hello
"hello"
end
end
# Your Object class
class Object
def in_english(&block)
instance_eval(&block)
end
def in_spanish(&block)
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
class Object
def say_hello
"hello"
end
end
# Your Object class
class Object
module InSpanish
def say_hello
"hola"
end
end
include InSpanish
remove_method :say_hello
def in_english(&block)
instance_eval(&block)
end
def in_spanish(&block)
instance_eval(&block)
end
end
in_english do
say_hello # => "hola"
end
in_spanish do
say_hello # => "hola"
endNow 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
class Object
def say_hello
"hello"
end
end
# Your Object class
class Object
module InSpanish
def say_hello
"hola"
end
end
include InSpanish
module InEnglish
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
def in_english(&block)
instance_eval(&block)
end
def in_spanish(&block)
instance_eval(&block)
end
end
in_english do
say_hello # => "hello"
end
in_spanish do
say_hello # => "hello"
endNow 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
class Object
def say_hello
"hello"
end
end
# Your Object class
class Object
module InSpanish
def say_hello
"hola"
end
end
include InSpanish
module InEnglish
expects_method = Object.instance_method(:say_hello)
define_method :say_hello do |*args|
expects_method.bind(self).call(*args)
end
end
include InEnglish
def say_hello
(@language || InEnglish).instance_method(:say_hello).bind(self).call
end
def in_english(&block)
@language = InEnglish
instance_eval(&block)
end
def in_spanish(&block)
@language = InSpanish
instance_eval(&block)
end
end
in_english do
say_hello # => "hello"
end
in_spanish do
say_hello # => "hola"
endThe 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.
1 comment:
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).
Reply DeleteIn 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)
Note: Only a member of this blog may post a comment.
[フレーム]