This article has been republished on Monkey and Crow.
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:
- How does ruby know it should run the tests when you execute your file?
- How does test unit know which methods are tests?
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:
- minitest defines
skip
which lets you skip a test and give a reason - There is a mocking library available in
mock.rb
- There is a tiny mocking library available in
mock.rb
- There is a small BDD testing library available in
spec.rb
Now aren’t you glad you took a moment to look at the standard library? Go forth and do something great.
Dude. Awesome!
Yes you are, thanks for your work on minitest.
Would you be open to some contract work? I’d love to have this level of doco throughout minitest (and others). Email me if you are.
Pingback: Getting to Know the Ruby Standard Library – Shellwords | End of Line
Pingback: Getting to Know the Ruby Standard Library – MiniTest::Mock | End of Line