Goodspeed IT Rants RSS

Rants from Goodspeed IT

Archive

Feb
21st
Sat
permalink

Ruby: hooks and metaprogramming 2

Here are some real life examples that come from Ruby-Concordion.

(A ruby active specification framework based on annotated HTML.  Try it out with: gem install concordion .)

The “inherited” method, to detect subclasses of ConcordionTestCase:

def self.inherited(subclass)

# Concordion config/housekeeping code omitted.
bind_test_method_to(subclass, config)
end

and the similarly defined “included” method, to detect classes that include the ConcordionTestMethods module:

def self.included(cmod)

# Concordion config/housekeeping code omitted.
bind_test_method_to(cmod, config)
end

Note that we’re keeping these methods DRY.  The method we want to add to both classes is the same: a test method that runs the association concordion spec. (Note: in practice be sure to capture the existing inherited/included() methods with alias_method, and delegate to the original implementation!).

Here’s the metaprogramming code that creates the appropriate test method:

def bind_test_method_to(subclass, config)
subclass.class_eval do
define_method :test_spec do
filename =  snake_cased_test_name(subclass.to_s)
parse_spec(filename,config)
failures  =   run_spec(filename, config)
report_spec(filename,config, failures)
end
end
subclass
end

This code opens the users class (which descends from ConcordionTestCase or includes ConcordionTestMethods) and defines a new method: “test_spec”.  Why is it done this way?

Because the framework can then:

  1. Infer the name of the specification (e.g. a class FooBarTest is bound to the spec “foo_bar.html”).
  2. Look for fixture code in the user’s class without the user doing anything but extending ConcordionTestCase or including ConcordionTestMethod: no plumbing code in user fixture code.
  3. Report errors with the appropriate context.  For example: specification errors in a concordion fixture UserTest come from UserTest.test_spec, not the parent class: ConcordionTestCase.
  4. Provide support for declarative configuration per user fixture.  For example: expected errors for a specific test case can define the method “expected_failure_count” to return the expected number of incorrect results in the spec.

I think it provides a nice example of the trade-offs involved in metaprogramming.  On the one side you have flexibility: the power to create a more friendly API than could be practically created without it.  On the other hand, the code is less direct and thus harder to read/maintain.

Readers interested in further context can view the complete Ruby-Concordion source code online.

The methods quoted come from “lib/concordion_test_methods.rb”.