Ruby is a particularly fun language thanks to metaprogramming. We’re allowed huge flexibility in re-wiring core pieces of a program even while it’s running. This power can be abused, but one place in which it’s a huge positive is testing.
“Testing” is a huge field full of strongly held philosophies, so let’s focus on the specific problem of unit testing: verifying that one particular piece of a program works as intended. Unit testing involves isolating precisely what behavior a component is responsible for and ensuring that it behaves as expected when the rest of the system is working correctly.
We’re allowed to assume that the rest of our system works, so wouldn’t it be nice if we could guarantee that? Stubbing lets us do this. With stubbing, we replace calls to external modules with a pre-defined result. Our tests no longer rely on external code and they’ll run faster as complex functions are replaced with pre-defined results.
For example, let’s say we’re building a system involving credit card
payments. Our system has two classes, Person and CreditCard:
class CreditCard
def process
... # complicated finance stuff
return successful_charge ? true : false
end
endclass Customer
# Charge, sending invoice on success or a notice on failure
def charge_and_notify
if @credit_card.process
send_invoice
else
send_error_notice
end
end
endWe’d like to write unit tests for Customer#charge_and_notify,
ensuring that we send the right email based on whether a charge
succeeds. We don’t want to actually charge cards during testing,
though, so how do we test this method? Let’s stub the
CreditCard#process function to make testing easier.
Common test libraries like
minitest have built in
support for stubbing (use those if you’re actually doing this). For
now, though, let’s remove the magic and write it ourselves. We open up
Object and add a class method called stub, which takes a method
name and desired result and ensures than any call to that method
returns that result:
class Object
def self.stub(method, result)
# Store the existing method first
alias_method "_custom_stubbed_#{method}", method
define_method method do |*args|
result
end
end
endThis is straightforward in Ruby. First we store the old method in a
safe place with the name _custom_stubbed_#{old_name}. Then we
overwrite the method to return the passed in result. Now we can do this:
> CreditCard.stub(:process, true)
> person = Person.new(card)
> person.charge_and_notify # sends a success email!When process is called on card, it will use our new method and
return true. That’s great, but we’re not done yet.
We’ve now overwritten process for all Credit cards for the entire
life of our ruby process. If we have another test which creates a
second person and credit card, that card will also return true for
every process call. It’d be better if we could limit the scope of
the stubbing in some way. Let’s improve stub to accept a block, and
only apply the stub during the block:
class Object
def self.stub(method, result, &block)
# Store the existing method
new_name = "_custom_stubbed_#{method}"
alias_method new_name, method
define_method method do |*args|
result
end
if block
begin
yield
ensure # Restore the old method outside of the block
alias_method method, new_name
end
end
end
endIf we’re given a block, we’ll run it and then restore the old method once we’re done.
Now we can do this:
> CreditCard.stub(:process, true) do
> person = Person.new(card)
> person.charge_and_notify # `CreditCard#process` stubbed out
> end
> person.charge_and_notify # `CreditCard#process` back to normalThis is pretty good, but let’s add one more piece of flexibility. What
if we wanted to stub process differently for different instances of
CreditCard? Let’s imaging we allow a person to link multiple credit
cards to their account, and the charge_and_notify function should
check them all. It’d be great to do this:
> card1.stub(:process, false) # invalid card
> card2.stub(:process, true) # valid card
> person.cards = [card1, card2]
> person.charge_and_notify # success, since card2 worksIn a prototype language like Javascript we’d just create new methods
directly on our card1 and card2 objects and everything would work.
In Ruby, we have to define these instance-specific methods using
define_singleton_method:
class Object
def stub(method_name, result, &block)
define_singleton_method method_name do |*args|
result
end
end
endNow calling card1.stub(:process, true) only affects card1. We can
still stub methods across all instances using CreditCard.stub from
before.
For completeness, let’s add block acceptance to our new instance-level
stubbing. This will let us do card1.stub(:process, true) { ... applies here ...
}:
class Object
def instance_stub(method_name, result, &block)
old_method = method(method_name)
define_singleton_method method_name do |*args|
result
end
if block
yield
define_singleton_method method_name, old_method
end
end
endAll this stubbing code along with tests and some documentation are available in a github repo. While you shouldn’t use my library in any production code, it serves as a good example of how simple Ruby’s metaprogramming makes seemingly complicated features.