Ruby Metaprogramming: Part II

Tweet

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_accessor :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

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://gmarik.info gmarik

    Hey!
    I think there’s a typo: `is_urakine?`
    Should be `is_ukraine?`

    Thanks for a good read!

    • http://unfinitydesign.com/ Nathan Kleyn

      Ah, good catch! Serves me right for turning spell checking off on the code snippets!

      Thank-you nonetheless for your kind compliments, I’m glad you enjoyed it!

  • http://www.methack.it/devblog mattia

    By the end, thanks for this read!

    • http://unfinitydesign.com/ Nathan Kleyn

      Mattia, I’m really glad you enjoyed it, thank-you for reading!

  • http://www.jakub.chodorowicz.pl/ Jakub Chodorowicz

    In the second code listing I think it should be attr_accessor :books (not attr_writer :books). Thanks for the interesting read!

    • http://unfinitydesign.com/ Nathan Kleyn

      You are totally spot on, good catch! I’ve edited that now. Thanks for reading, and glad you enjoyed!

  • wiseland

    “Once it’s an Array, we us the Enumerable method any?…” should be
    Once it’s an Array, we USE the Enumerable method any?…

    Thanks for the article!