Monday, October 12, 2009

Unit testing in Ruby

Once upon a time, a Ruby programmer would more or less assume that test/unit was all there was to unit testing in Ruby. It followed the paradigm laid down by JUnit, and that was that. Nowadays, specification (as opposed to unit testing) frameworks like RSpec are all the rage. Doing some serious Ruby coding for the first time in ages, I turned to test/unit out of habit, but found it just as unwieldy as I always remembered it. It fits the way I think about unit testing (that is, create an object and assert a bunch of things). Looking for alternatives, I couldn't believe how much has changed. It seems every arsehole has their own testing framework now!

test/unit is now maintained as a gem and is having a bunch of improvements added to it. It was nearly replaced in the Ruby standard library by minitest but the author of that was too arrogant to make the marriage work. dfect has a nice minimal API and drops you into a debugger when something fails. Ara Howard, always an author of excellent libraries, created testy, a minimalist and very opinionated approach. And there are a bunch of nimble gems that tinker juts a little with test/unit (e.g. to add contexts or what have you). There's a long list to look at here.

So what have I gone with? Drumroll... test/unit! I installed the v1.2.3 gem, which is exactly the same test/unit as I left it years ago. I've also installed turn, to give nicer output, and hacked it to give even more colour.

This setup is doing well at the moment, but I've given some thought to features of my own testing framework, should it ever eventuate:

  • Simple approach, like test/unit (but also look at dfect and testy).
  • Less typing than test/unit.
  • Colourful output, drawing the eye to appropriate filenames and line numbers.
  • Stacktraces are filtered to get rid of rubbish like RubyGems's "custom_require" (I do this already with my mods to turn).
  • Easy to select the test cases you want to run.
  • Output like turn.
  • Optional drop-in to debugger or IRB at point of failure.
I'm hoping not to create a testing framework anytime soon, but am saving this list here in case I want to do so in the future.

And finally, although I like elements of the spec approach in theory, in practice I've never warmed to them. Before creating a framework, it would be wise to try out that approach properly, to see what aspects of it I'd like to implement.

Update: Two more features to add. Colour-coding the "expected" value (green) and the "actual" value (red) in the case of a failure (introduced in test-unit-2.0.4); code-based filter of tests to be run within the class (e.g. class TestSomething < TestCase; def filter; /789/; end; ...; end). That's easy to modify as you repeatedly run tests in that class and want to focus on the output of just a couple of them.