Getting to Know the Ruby Standard Library – MiniTest

Ghetto or not, if you want to use or contribute to the ruby standard library, getting to know the source is essential. We will look at one of the libraries everyone should be acquainted with, test/unit. There are dozens of testing frameworks in ruby, but this is the one you know everyone will have available. I’ll assume you’re using ruby 1.9.x, because it’s awesome.

Overview

Test/Unit is a ruby implementation of original xUnit architecture. In general you write your tests by extending Test::Unit::TestCase, adding methods that look like test_*something*, and then you can run that file directly with ruby my_neat_test.rb. Since it’s best to have a purpose when learning, we’ll try to answer two questions:

Test/Unit

To get started, open up Test/Unit (qw test if you have Qwandry installed).

Looking at unit.rb we immediately see something odd:

# test/unit compatibility layer using minitest.
require 'minitest/unit'
require 'test/unit/assertions'
require 'test/unit/testcase'

In ruby 1.9 minitest was swapped in to replace test/unit. The code in test/unit is just here to make sure all the tests behave the same way in ruby 1.9 as they did in ruby 1.8. If you scroll down to the bottom of unit.rb, you will find this:

MiniTest::Unit.autorun

This is going to get called any time that you require test/unit, so lets find out what it does, and I suspect we can unravel our first question.

Minitest

There isn’t much in test/unit so lets take a look at minitest since that’s what ruby 1.9.x is really using (qw minitest). Since we saw that autorun is called whenever we require test/unit, autorun.rb sounds like a good place to start looking. The contents of this file?

MiniTest::Unit.autorun

Ok, well we have three more files left, so unit.rb is probably another good place to look. A quick search for autorun will yield:

class Unit
  #...
  def self.autorun
    at_exit {
      next if $! # don't run if there was an exception
      exit_code = MiniTest::Unit.new.run(ARGV)
      exit false if exit_code && exit_code != 0
    } unless @@installed_at_exit
    @@installed_at_exit = true
  end

We see that at_exit is being called. If you haven’t run across this before, so a quick peak at the docs for ruby-core shows that this block will be executed right before ruby exits. Notice that minitest calls MiniTest::Unit.new.run(ARGV) inside the block? Now you know how the tests get run. You can also see that @@installed_at_exit is in there to prevent your tests from being launched more than once. Neat stuff, one question down.

Next up, how does it know which tests to run? We saw that MiniTest::Unit.new.run(ARGV) is going to get called, so that’s a good place to start.

def run args = []
  @verbose = args.delete('-v')

  filter = if args.first =~ /^(-n|--name)$/ then
             args.shift
             arg = args.shift
             arg =~ /\/(.*)\// ? Regexp.new($1) : arg
           else
             /./ # anything - ^test_ already filtered by #tests
           end
  #...

We can see that this code starts out by using the options from ARGV to configure MiniTest::Unit. Let’s look at filter it will either end up being the name of a test, or a regexp that matches everything. The comment looks promising too. Hopping down a few lines we see that filter gets used:

run_test_suites filter

Looking ahead we see that TestCase.test_suites seems to be enumerable, and each suite has a set of test methods.

def run_test_suites filter = /./
  #...
  TestCase.test_suites.each do |suite|
    suite.test_methods.grep(filter).each do |test|
      inst = suite.new test
  #...

We’ll ignore the test_suites for now and look into the test_methods code, this is probably where minitest finds all of the tests you wrote.

def self.test_methods
  methods = public_instance_methods(true).grep(/^test/).map { |m|
    m.to_s
  }.sort
  #...

And there we go, your test inherits from TestCase and TestCase.test_methods introspects on itself looking for all the public methods that start with the word test. So now we’ve answered our questions, and if you were paying attention, we learned a few things that might be useful.

Recap

In ruby 1.9 test/unit was replaced with minitest. It turns out that minitest registers an at_exit hook, and that it introspects on its own methods to figure out what to run. Now while you were in the code, perhaps you noticed few other interesting things that might be of use later:

Now aren’t you glad you took a moment to look at the standard library? Go forth and do something great.

blog comments powered by Disqus
Monkey Small Crow Small