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
:
We’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:
This 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:
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:
If we’re given a block, we’ll run it and then restore the old method once we’re done.
Now we can do this:
This 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:
In 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
:
Now 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 ...
}
:
All 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.