[Previous entry: "Eclipse 3.0RC1 and JUnit"] [Next entry: "A comment on "Domain-Centric Programming""]
06/07/2004: "Parameterized Test Case, Ruby style"
Curt Sampson contributed a neat idea to the junit Yahoo! group about implementing the Parameterized Test Case pattern idiomatically in Ruby. The result is startling, compared to the corresponding JUnit code.
Without further ado, I'll let Curt have the floor:
Anyway, I've appended a little example I was playing with this morning where I ended up making a test suite that was just a hash of inputs and expected results, something you'd typically have to build up a parameterized test case for. The key code ends up being, instead, just:
fibtest_hash = { 0 => 0, 1 => 1, 2 => 1, 3 => 2, 4 => 3, 5 => 5, 6 => 8 }
def fibtest_hash.calculate_result(input)
Fib.new.fib(input)
end
fibtest_hash.extend(TestEachInputExpectedPair)
Test::Unit::UI::Console::TestRunner.run(fibtest_hash)
Of course, that object doesn't have to be a Hash. It might be, for example, a database query that pulls data from test runs against an oracle, or even the oracle itself:
suite = SQLQuery.new("SELECT input, result FROM old_test_run")
def suite.calculate_result(input)
...
end
suite.extend(TestEachInputExpectedPair)
Test::Unit::UI::Console::TestRunner.run(suite)
Unfortunately, I'm out of time today to think about this further....
Here is the code that Curt provided for TestEachInputExpectedPair, which provides a custom suite method:
module TestEachInputExpectedPair
def suite
suite = Test::Unit::TestSuite.new(self.to_s)
each_pair { |input, expected|
suite << InputAndExpectedTestCase.new(input, expected) { |i|
self.calculate_result(i)
}
}
return suite
end
end
Any object that implements the method each_pair can extend this module to mix in the custom suite creation ability. In JUnit, you have to do two things:
- Add the fixture data (the input and expected value) to the test case class's constructor.
- Implement the custom
suite()method, which is a cut-and-paste loop over the test data creating a test case from each row of data.
With this approach, the custom suite extraction is written Once And Only Once! Now here is the test case class:
class InputAndExpectedTestCase < Test::Unit::TestCase
def initialize(input, expected, &calculate_result)
super("run_test")
@input = input
@expected = expected
@calculate_result = calculate_result
end
def name
return "input #{@input}"
end
def run_test
assert_equal(@expected, @calculate_result.call(@input))
end
end
This is quite straightforward, and the result is powerful. Curt has reduced all that extra typing to these basic steps:
- Create a collection mapping inputs to expected values.
- Implement
each_pairon the object. - Implement
calculate_resultto provide the engine of the test, which in JUnit you would do by overridingrunTest(). - Have the test data collection extend
TestEachInputExpectedPair. The resulting object becomes the custom-built suite for the Parameterized Test Case.
Isn't dynamic typing great?! I need to write more Ruby.
