-
Get a monthly update on best practices for delivering successful software.
In this series of posts, I’m going to walk through the implementation of an interesting Ruby problem, namely, using metaprogramming to create a small DSL-like syntax for specifying tests.
Here’s the problem as I notice it in my own unit testing: I tend not to go back and ensure that multiple input values for a method work — worse, I don’t often check for error conditions or possible corner cases.
The main reason for this, I think, is a) wanting to move on to the next thing, and b) not wanting to take the time to refactor the test to make adding another test call simple. What I really want is something where the test is essentially pre-factored, so that adding a new call is easy. The newest test frameworks on Java use function decorators to specify multiple sets of data over the same test.
In Ruby, we can achieve the same effect via metaprogramming, which allows a clean enough API to make specifying new tests easy.
I think I want the syntax to look like this:
testbed "call my test function" do |input|
subject = MyObject.new(input)
subject.method_to_test
end
verify_that(1).returns("fred")
verify_that(2).returns(3)
verify_that(0).raises(Exception)
In other words, using something similar to the way Rake matches descriptions to tasks. Define a testbed to perform some assertions on a given set of data, then assert any number of different instances of the test using verify_that. Each verify_that is automatically associated with the most recent testbed, and gets converted to an actual test method called when the test class is run.
So, how do you implement this, and how do you test it? Here’s the first test I wrote:
require 'test/unit'
require '../lib/testbed'
class TestbedTest < Test::Unit::TestCase
class FakeTestA < Test::Unit::TestCase
def test_something; true end
end
class FakeTestB < Test::Unit::TestCase
def test_something; true end
end
def test_creating_a_testbed
FakeTestA.testbed("this is a fake testbed"){ |a| a * 2 }
assert_equal("this is a fake testbed", FakeTestA.current_testbed.name)
assert_equal(4, FakeTestA.current_testbed.block.call(2))
FakeTestB.testbed("a different fake testbed") { |a| a * 3 }
assert_equal("a different fake testbed", FakeTestB.current_testbed.name)
assert_equal(6, FakeTestB.current_testbed.block.call(2))
assert_equal("this is a fake testbed", FakeTestA.current_testbed.name)
assert_equal(4, FakeTestA.current_testbed.block.call(2))
end
end
First off, I created a pair of fake test classes to use later on (I gave them a single test to prevent Test::Unit from complaining when I run the tests). The actual test method explicitly calls the testbed method on the fake classes, mimicking the actual usage shown earlier. What I’m testing here is that the testbed call places an object in a current_testbed slot so that it can be referenced by later calls to verify_that.
(I test it on both fake classes to verify that multiple classes can hold separate values in the class variable at the same time).
The first round of implementation looks like this:
module Test module Unit class TestCase class << self attr_accessor :current_testbed def testbed(string, &block) self.current_testbed = Testbed.new(string, block) end end end class Testbed attr_accessor :name, :block def initialize(name, block) @name = name @block = block end end end end
So far, this is pretty simple. I’ve created a Testbed class to hold a name and a block. I’ve also opened up the TestCase class to add the class method testbed. Right now, that method does nothing more than store the string and the block in a Testbed instance, and stores that instance in a class-level attribute called current_testbed.
The way in which the class method and class attribute are set up is very Ruby specific. Each object in Ruby has a special attribute called a singleton class. If filled, the singleton class can have it’s own methods and data, and that class is checked for method names before the object’s own class or any of it’s superclasses. This allows an individual Ruby instance have unique behavior — different from other instances of the same class — just by placing different methods in it’s singleton class.
To define a singleton class, you use the form:
class << obj end
Where obj is the instance whose singleton class you are defining. (An instance only has one singleton class. If you define multiple blocks for the same object, they are all added to the same singleton class.)
In this case the object in question is self, which when referenced outside a method definition refers to the class being defined. In Ruby, classes are objects just like anything else, and they can have singleton classes. This gets a little strange when dealing with instances of the class Class, but the basic idea is that adding methods to the singleton class is equivalent to adding instance methods to the class itself.
The testbed method, which is defined in the singleton block, becomes an instance method of the class TestCase, which means it can be invoked as a Class method as in:
FakeTestA.testbed("this is a fake testbed"){ |a| a * 2 }
The nice thing about using the singleton format, rather than using regularly defined class methods and attributes, is that each subclass of TestCase will get it’s own singleton class, and therefore each can have its own current_testbed attribute, which is the functionality we want.
That’s going to be it for Part 1. Next time, I’ll implement the verify_that method chain.
Related posts:
Topics: Ruby on Rails, Test Driven Development, Unit Tests