A MiniTest::Spec Tutorial: Elegant Spec-Style Testing That Comes With Ruby

By Peter Cooper

Despite RSpec's awesomeness, Test::Unit remains the most popular Ruby testing tool out there outside of Rails apps. I've recently been code walking through a lot of Ruby libraries for my Ruby Reloaded course and the typical arrangement is Test::Unit, sometimes coupled with Shoulda or Contest for some extra syntactic sweetness.

Part of the reason for Test::Unit's enduring popularity is its presence in the Ruby standard library but, also, its general 'lightness' and speed. When you're writing a large app, using a powerful full-featured system like RSpec has significant benefits (particularly stakeholder involvement in writing the specs). But when you're working on a library that might spread far and wide and is aimed solely at developers, the pros of Test::Unit shine through.

Enter MiniTest

With Ruby 1.9, however, MiniTest entered the standard library. require 'test/unit' still works in Ruby 1.9 but it's provided through a compatibility layer on top of MiniTest, so if you're using require 'test/unit' in Ruby 1.9, you're really using MiniTest under the hood. It's possible to switch to using MiniTest::Unit directly without much effort (a few changed assertions and minor additions) but more exciting is the inclusion of MiniTest::Spec , a contextual RSpec-esque syntax, ready to run out of the box with Ruby 1.9.

Note: Ruby 1.8 users can run gem install minitest to get MiniTest too but it's not part of the standard library there.

What Does MiniTest::Spec Look Like?

Let's start with a ridiculously simplistic Test::Unit style test:

require 'test/unit' class TestArray < Test::Unit::TestCase def test_array_can_be_created_with_no_arguments assert_instance_of Array, Array.new end def test_array_of_specific_length_can_be_created assert_equal 10, Array.new(10).size end end

Nothing too unusual there, I hope. Let's convert it to MiniTest::Spec:

require 'minitest/spec' require 'minitest/autorun' describe Array do it "can be created with no arguments" do Array.new.must_be_instance_of Array end it "can be created with a specific size" do Array.new(10).size.must_equal 10 end end

It's a matter of style and opinion, of course, but I prefer the latter version. MiniTest::Spec brings RSpec-esque matchers and contexts right into the Ruby 1.9 standard library and I hope it will start to make significant inroads into the library and developer tool test suites, replacing raw Test::Unit.

(For the purists out there, minitest/spec is implemented in a single ~300 line Ruby file. It's not a framework and it's easy to walk through the code in 10 minutes. Minimal magic!)

MiniTest::Spec's Matchers / Expectations

Previously we just did a check for equality and an object's class, but you're going to want to go a bit further. Here are MiniTest::Spec's key expectations:

obj.must_be(operator, expected) - for example, 10.must_be :< , 11

- for example, obj.must_be_close_to - the equivalent of assert_in_delta

- the equivalent of obj.must_be_empty - Fails unless obj.empty?

- Fails unless obj.empty? obj.must_be_instance_of(klass) - Fails unless obj.class == klass

- Fails unless obj.must_be_kind_of(klass) - Fails unless obj is of class klass or klass is one of its superclasses.

- Fails unless obj is of class klass or klass is one of its superclasses. obj.must_be_nil

obj.must_be_same_as - tests for true object equality

- tests for true object equality lambda {}.must_be_silent

obj.must_be_within_delta

obj.must_be_within_epsilon

obj.must_equal(other) - Does a ==/eql? comparison between two objects.

- Does a ==/eql? comparison between two objects. obj.must_include(other)

obj.must_match(regex) - A regular expression match, e.g. "hello".must_match /w+/

- A regular expression match, e.g. lambda {}.must_output(stdout, [stderr..]) - The block should have certain output on stdout or stderr. Set stdout to nil just to check stderr.

- The block should have certain output on stdout or stderr. Set stdout to nil just to check stderr. lambda {}.must_raise(exception)

obj.must_respond_to(message)

obj.must_throw(sym)

The above are all positive expectations but the opposite ones are easy to build as in most cases you can switch must with wont . For example:

wont_be

wont_be_empty

wont_be_instance_of

wont_be_kind_of

wont_be_nil

wont_be_same_as

wont_equal

wont_include

wont_match

wont_respond_to

Note: If you look at the source for minitest/spec.rb you'll see that the expectations map directly to MiniTest::Unit assertions.

Running MiniTest::Spec Specs In Your Ruby Project / Library

Generally, running MiniTest::Spec tests can use the same mechanisms as you would for Test::Unit tests, so there's not much to do if you're already up to speed with T::U.

To get things going with rake just bring rake/testtask into your project's Rakefile , if it's not already there, and make some tweaks:

require 'rake/testtask' Rake::TestTask.new do |t| t.libs.push "lib" t.test_files = FileList['test/*_test.rb'] t.verbose = true end

You'll want to tweak the glob in FileList when you follow a different convention for filenames (e.g. test/test_*.rb or specs/*_spec.rb ). It's easily updated and gives you rake test for the running.

Give It A Try

So next time you're starting on a new library and you're focusing on Ruby 1.9 (it's about time :-)), give MiniTest::Spec a try. You get a neat testing syntax and it's all part of the standard library. (If Ruby 1.8 compatibility is still important, of course, you could even just add 'minitest' to your Gemfile.)

If Ruby 1.9 interests you specifically, check out The Ruby 1.9 Walkthrough, a mega screencast aimed at Ruby 1.8.7 developers who want to learn all about what's new, what's gone, and what's different in Ruby 1.9.2 and 1.9.3.

Further Reading

Peter here! :-) I've been running an online course called Ruby Reloaded over the past couple of months for intermediate Ruby developers who want to revise the basics and pick up some new tricks. The next run will probably be in October or November so if you want to be on the waiting list or just learn more about the course, click here.