Welcome back to Ruby Metaprogramming! In part one we looked at what Metaprogramming is and how it works; we explored deep into the internals of Ruby’s method lookup system and walked through how creating Singleton Classes fits into that mechanism. Now for the good part: applying it all.
Mocking objects for testing
Some of the most useful features of Ruby’s metaprogramming have been shown off countless times in the vast array of testing frameworks available. Whilst a lot of these frameworks use various forms of metaprogramming to create their APIs, we’re going to explore one of the most important uses of metaprogramming used in Ruby test frameworks: metaprogramming for mocking and stubbing.
You can probably start to draw conclusions about how this might work given what you now know about metaprogramming. Let’s get started with a nice, simple example:
class Book end class Borrower end
We’re going to pretend we’re managing a library for a day, and that we need to record what books are borrowed out and by whom. We will also need to know if the person trying to borrow a book as reached their limit of books loaned out. To do that, we make two classes: the Book
class, and the Borrower
class.
class Borrower attr_writer :books def initialize @books = [] end end
It goes without saying that the Borrower
is allowed to borrow many books, so above we have given each instance of Borrower
a class variable @books
to hold the Books
they currently have out. We’ve added an attr_accessor
so we can access this at borrower.books
and assign to it using borrower.books=
. It would also be useful to know how many books the Borrower
has out, so let’s go ahead and add a method to do that easily:
class Borrower # ... def books_on_loan @books.length end # ... end
Finally, we need to know if a Borrower
is allowed to loan out any more books (this library is a bit strict, so we only allow people to loan out 5 books at a time); a simple method can achieve just that:
class Borrower # ... def can_loan? @books.length 5 end # ... end
Now because you’re a good coder, in the real world you probably would have written some tests before you started to code these methods. One of the things you’d probably start to realise while doing this is that in order to test the :can_loan?
method you have to have a collection of Books
assigned to the Borrower
. You could achieve this by adding some Books
to your Borrower
before you test the actual method, but this way of testing becomes painstakingly verbose and will fail if there are any problems with the Book
class when creating these instances. Here’s where metaprogramming comes to the rescue:
describe Borrower do before :each do @borrower = Borrower.new end describe "can_loan? performs correctly" do it "returns false if equal to or over the limit" do @borrower.books.instance_eval do def length 5 end end @borrower.can_loan?.should == false end it "returns true if under the limit" do @borrower.books.instance_eval do def length 1 end end @borrower.can_loan?.should == true end end end
I’ve used RSpec here, but the principles will apply just the same way in whatever testing framework you choose. Through the power of metaprogramming, you no longer have to create a collection of Books
and hold your test at the mercy of external dependencies; instead, you can override the very methods your class method uses to force them to return the values you are testing for. You can begin to imagine the power of this methodology when you realise that you can stub dates and times, IO activity, database records, external API calls; the list goes on.
So the next time you find yourself creating a lot of external dependencies or relying on additional classes in your tests, consider adding a sprinkle of metaprogramming into the mix.
One thing to note before moving onto a more interesting example is that mocking and stubbing of objects is a process fairly extensively encapsulated by lots of fantastic gems. You’ll find projects like FlexMock and RSpec Mocks invaluable for keeping your metaprogram sprinkled tests DRY.
Dynamic methods
Libraries like ActiveRecord have come to be known for their interesting use of metaprogramming, allowing them to create methods that are generated on the fly based on user data. Let’s see if we can implement something similar.
By defining the method_missing
method in a class, any unsuccessful calls to methods within that class or it’s inheritance chain will automatically call the method_missing
method you have defined. Combined with the power of instance_eval
and class_eval
, you have a recipe for incredible DSLs and flexible APIs. This combination can not only be used to allow dynamic methods, but can also prevent having to program repetitive methods.
As an example, we’ll attempt to create a Country
class which responds to a method is_countryname?
where countryname
is the name of the country you’re testing for. As a step further, we’ll also make it respond to is_countryname(_or_countryname...)?
where the _or...
can be repeated indefinitely to match a list of possible countries.
Let’s start by creating our Country
class:
class Country attr_accessor :name def initialize(name) @name = name end end
The Country has a name
attribute which must be set when creating an instance of the class. We also have a very simple initializer method which gives it an initial value. Now, let’s implement our method_missing
method:
class Country ... COUNTRY_QUERY_REGEX = /^is_((?:_or_)?[a-z]+?)+?$/i def method_missing(meth, *args, block) if COUNTRY_QUERY_REGEX.match meth.to_s self.class.class_eval -end_eval def #{meth} self.__send__ :check_country, "#{meth}" end end_eval self.__send__(meth, *args, block) else super end end end
This looks far more complicated than it is, so let’s go through it line by line. On line 5, we start the code for our method_missing
function by checking if the method being called matches a regular expression; don’t worry too much about the Regexp
itself as all that matters is that it matches things of the form is_something_or_somethingelse...?
(eg. is_italy?
and is_italy_or_ukraine?
).
Once we know that the method we’re trying to call matches the pattern we’ve set up, we can then launch ourselves into a now familiar class_eval
statement on line 6. We use the heredoc syntax to effectively create a nicely formatted multiline string, but you could just as easily have put the entire block of code here on one line inside a string. Inside the block, we define a method with the same name as the one being called, and we have it call the method which will do the heavy lifting on line 8 (which we’ll define in a minute). We create the method which is missing within this handler so that it’s quicker on subsequent calls, but you could have called the check_country
method and not created the method if you wanted to keep it simple. Once we’ve defined it, we call the method we just created.
If the pattern didn’t match when method_missing
was called, we call super
on line 13; this is important, as if you don’t call super
here no other classes in the inheritance chain will be asked if they can answer to the missing method.
Now to define the contents of our check_country
method:
class Country ... private def check_country(query) countries = query[3..-2].split("_or_") countries.any? { |s| s == @name } end end
Inside this method we split the query into an Array
, separating them by the “or” bit in between; this will use the entire value of query
if it didn’t contain any “or“. Once it’s an Array
, we us the Enumerable
method any?
to check if any of the items in the Array
match the name of the country that our class instance represents. As this is the last call of the method, it’s return value is also the return value of the function.
Now let’s give it a go:
italy = Country.new("italy") italy.is_ukraine? # = false italy.is_italy? # = true italy.is_ukraine_or_italy? # = true italy.is_ukraine_or_australia_or_portugal_or_italy? # = true
By now you’ve probably realised the power of what we’ve created. To make these methods by hand would be either extremely laborious or difficult to maintain, or impossible. By combining metaprogramming techniques with our method_missing
callback, we’ve created an extremely expressive and beautiful API that is DRY and easy to maintain.
Conclusion
Although it looks complicated, what we have achieved in this article is something that would often be very difficult or labourious to code normally, and that is the beauty of metaprogramming. Things that can seem hard or even impossible can often be coded using simple metaprogramming techniques.
You have learned about the Singleton class in Ruby and how it alters the very fabric of the language by adding a new level of lookups to each method call, and we have applied this powerful technique to produce code that can turn everyday solutions into elegant and reusable patterns.
Further Reading
- Understand the Ruby singleton further with Peter Jones from Contextual Development.
- Get the Pickaxe of metaprogramming, “Metaprogramming Ruby”, written by Paolo Perrotta.
- Dave Thomas has a brilliant video series on PragProg called The Ruby Object Model and Metaprogramming.