The Pragmatic Duo suggest that programmers should learn a new [programming] language every year. One approach is to apply learning tests. I saw a talk on the subject by Justin Halloway last week. It looked promising so I decided to give it a go.
At the surface, learning tests are no different from the unit tests used in test-driven development (TDD). Write a test case, then the code that makes the test pass.
Being as fashion conscious as anyone, I've been looking into Ruby, a fun language that feels a bit like object-oriented Perl. The text of a book by the pragmatic duo is available on the web. I was able to get a hard copy through a discount bookseller.
Following standard advice, I kept the test cases under version control. They serve two purposes: as documentation of my learning and as change detectors for the future. When the next version of the language is released, I can run the tests against it to see what has changed.
From TDD we know that the best tests run automatically, without supervision. All we need to know is whether they pass or fail. If there is an xUnit module for the language, that's usually a good way to automate tests, especially for people already familiar with JUnit. Even if there is no xUnit available, a simple driver program that runs test cases is not hard to cook up. Ruby's version of the xUnit module is Test::Unit (part the standard distribution as of version 1.8.0).
require 'test/unit' class SimpleTest < Test::Unit::TestCase def test_simple # tests with asserts go here end end
It was time to write some tests and exercise them. There are plenty of exercises that can be useful. I was looking for something relatively simple, self-contained and well defined. One of the classic test-first exercises is to parse Roman numerals. The test cases looked something similar to the following.
assert_equal(1, @roman.to_int("I")) assert_equal(6, @roman.to_int("VI")) assert_equal(39, @r.to_int("XXXIX")) assert_equal(84, @r.to_int("LXXXIV")) assert_equal(384, @r.to_int("CCCLXXXIV")) assert_equal(1999, @r.to_int("MCMXCIX"))
I wondered what should happen with garbage input. How should the code handle errors? It was time to explore what kind of exception handling the language offers. A good tester takes the role of an adversary, trying to break the code by looking for edge cases, where the input is almost, but not quite, valid. I am not a professional tester, so my cases turned out fairly straightforward.
assert_raises(RomanParseError) { @r.to_int("MMDCCLIS") } assert_raises(RomanParseError) { @r.to_int("XXL") }
Parsing Roman numerals is fairly easy. Personally, I find it slightly harder to go in the other direction, from native integers to roman numerals. This became the second exercise.
assert_equal('I', @r.to_roman(1)) assert_equal('III', @r.to_roman(3)) assert_equal('VIII', @r.to_roman(8)) assert_equal('XIX', @r.to_roman(19)) assert_equal('XL', @r.to_roman(40)) assert_equal('XLIV', @r.to_roman(44)) assert_equal('LXXVII', @r.to_roman(77)) assert_equal('CLXXVII', @r.to_roman(177)) assert_equal('DCCXCIX', @r.to_roman(799)) assert_equal('MDCCLXXVI', @r.to_roman(1776))
One nice feature of TDD is the way you start with an easy problem. At first I wrote only the first assertion (to convert 1 to "I") and wrote the shortest piece of code that would pass the test. However, once I had the really trivial cases done, I found myself cutting and pasting code. The rules for "X", "C" and "M" are basically the same, modulo the actual values. The same observation applies to the rules for "V", "L" and "D".
Cutting and pasting or indeed any repeated patterns in the code are a signal for refactoring. By trying to factor out the commonality I found myself exploring the language, using the index of the book heavily.
Once conversions both ways were working, it made sense to feed the two converters to each other. A number taking the trip back and forth should return unchanged. I chose to try every number from one to 2511.
def test_simple() 1.upto(2511) do |i| assert_equal(i, @roman.to_int(@roman.to_roman(i))) end end