I don’t know the key to success, but the key to failure is trying to please everybody.

Menu

Exploring Metaprogramming Idioms similar to RSpec

I have been learning Ruby for 2-3 months. At the beginning, I was very shocked by some approaches being used there.
Recently, I have been very excited about the powerful meta-programming features working very reliably. By the way, a few years ago, I tried to explain to people how difficult it is to deal with such dynamic languages like Javascript (and other ones as well) and how lucky Java programmers are because the have a compiler.

I think I have to audit my opinion since I made some experiments with Rails and I was surprised about the beauty of ActiveRecord and RSpec. The latter one emphasizes the power of Ruby by defining it’s own Domain Specific Language (DSL). I got very curious and spent some time with exploring several Ruby mechanisms which make possible constructs like:

describe Something do
it "should be possible!" do
...
end
end

I decided to write my own DSL in order to learn more about the mechanism making all that possible. The source code is available via GitHub (see below).

I had some knowledge about methods and blocks so that it wasn’t difficult to guess that there are two (global?) methods being used as keywords
(from the user’s point of view):

describe

it

Everything that follows the (built-in) keyword “do” is a block.

After a while I had an idea about the purpose of this new DSL. It has to support people choosing an appropriate candidate for a job ;-).

To avoid some conficts to RSpec I selected different “keywords”. The usage of the resulting (small and humble) framework should look like this:

specify Candidate do
she "should be young and motivated" do
@c = Candidate.new
@c.name = "Anonymous"
@c.age = 30
@c.should_not be_old
end
end

I was done with that after a couple of hours. The framework consists of 80 (!) lines of code (including empty ones). I’m sure that there are many developers who are able to provide a better solution. For now, though, I’m very satisfied with this solution.

Let’s take a look at the architecture:

Implementing “specify”:

The (global?) method specify is able to process a “thing” that normally is (and must be in this implementation) a Class object. The argument thing and the passed block are stored in a global Hash @things. Consider that the block is converted into a Proc object to be evaluated later (well-known as Deferred Evaluation). Additionally, we use the beautiful syntax of << to define accessors for variables descs (used for storing test descriptions) and results (used for storing test results).

Each time when something is “specified” by the method “specify”, all the specifications are stored in a Hash and finally evaluated in context of the class. The nested “he”/”she” keywords are evaluated in context of the class as well (see the nested loop). Why? If you consider the usage snippet you can see that we invoke the method called be_old. But this method doesn’t exist at all. Method invocations which look like “be_old” are sometimes described as “Class Macros” (see also Metaprogramming Ruby Book by Paolo Perotta). So I thought it’s a good idea to evaluate the nested he/she blocks in Class’s scope as well.

Implementing “he”/”she”:

That’s pretty simple. Since, the block passed in “specify” is evaluated in scope of the Class Candidate, the keyword “self” is the Class itself when Ruby enters “he” or “she”. As you can see the Class itself (here: Candidate) has got the attribute “descs” that has been created before inside “specify” (by the strange syntax <<). Here is where I store the mapping of descriptions ("should be young and motivated") and the related Procs (for deferred evaluation).

Note: “he” and “she” have the same method body (just for simplicity I set gender-related details aside ;-)).

Implementing “inject_sugar”:

Maybe this is the most puzzling part of the solution but this is what metaprogramming is about: writing code that writes code.
This is how methods like should, should_not and be_old … are born.

The method “inject_sugar” is defined in the same scope like “specify”. It receives the Class itself as an argument. This is necessary for defining some syntactic sugar like dynamic instance and class methods.

The methods “be_old”, “be_young”,… are executed in the Class’s scope and they aren’t defined. So we’ll need a mechanism that is able to catch invalid method invocations silently. This is what method_missing is for.Note: we could get rid of that when using :be_old or be_old but instead we do that the same way that RSpec does 😉
My implementation of method_missing returns the name of the method itself. After all, the return value of “be_old” is passed as an argument for should and should_not.

The only difference between them is that an additional parameter (true or false) is added because “should” checks for “true” and “should_not” checks for false.

The “validate” method detects the real instance method name. The create class macro “be_old” becomes “old?”, “be_young” becomes “young?” and so on. This is the mechanism of detecting the ordinary method to be called and tested.

Depending on the final check the method returns “SUITABLE…” or “NOT SUITABLE…”. Remember the dealing with deferred evaluation inside “specify”. This is the place where the value is returned and extended by the prefix “SUITABLE…” or “NOT SUITABLE…”.