Want to stay on top? Ruby Weekly is a once-weekly e-mail newsletter covering the latest Ruby and Rails news.
     Feed Icon

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

By Peter Cooper / September 2, 2011

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
  • obj.must_be_close_to - the equivalent of assert_in_delta
  • obj.must_be_empty - Fails unless obj.empty?
  • obj.must_be_instance_of(klass) - Fails unless obj.class == klass
  • obj.must_be_kind_of(klass) - 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
  • 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.
  • obj.must_include(other)
  • obj.must_match(regex) - A regular expression match, e.g. "hello".must_match /w+/
  • lambda {}.must_output(stdout, [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.

Comments

  1. Oriol Gual says:

    Instead of:

    t.test_files = FileList['specs/*_spec.rb']

    I'd rather go with:

    t.pattern = 'specs/**/*_spec.rb'

    This way one could have subfolders to organize the specs. Also, remember to add the spec/test folder to lib in order to load a test/spec_helper:

    t.libs.push 'specs'

  2. Martijn says:

    Cool, I'm going to convert the specs for my new gem to MiniTest :) I like how it supports both TU and spec style.

  3. Andrés Botero says:

    I think you can leave the minitest/spec require out if you include minitest/autorun.

  4. Peter Cooper says:

    Yeah, I think you can too, as the other demos of it I've seen do that. For some reason I like being explicit in my choice but totally! :-)

  5. Peter Cooper says:

    Oriol: I prefer your approach, I'll update the article soon.

  6. Neeraj Singh says:

    This video by Ryan Davis has a good overview of MiniTest.

    http://confreaks.net/videos/618-cascadiaruby2011-size-doesn-t-matter

  7. Marc says:

    The only downside is, that there currently doesn't seem to be a way to generate junit xml output using minitest. The ci_reporter gem doesn't support it and there doesn't seem to be the concept of formaters for minitest :(

  8. Sam Livingston-Gray says:

    I'm a huge fan of nested context blocks for testing, but I've never gotten over my dislike of RSpec's .should matchers.

    So I feel obligated to point out that you don't have to use the .must_* and .wont_* method assertions -- MiniTest is perfectly happy to execute assert_* and refute_* methods inside test blocks. If I remember correctly, MiniTest::Spec is basically just a thin wrapper that generates plain-vanilla MiniTest::Unit classes and calls for you.

    For the curious, I recommend watching Ryan Davis' recent talk about MiniTest; a 33-minute video is available at: http://confreaks.net/videos/618-cascadiaruby2011-size-doesn-t-matter

    (Also, I continue to be disproportionately amused and grateful that Ryan came up with two sets of words that have the same number of letters (assert/refute and must/wont); I've used assert/deny in the past and am always mildly annoyed that mixing the two when doing several related assertions in a row throws off the vertical alignment of my code.)

  9. Francesc Esplugas says:

    Peter, Oriol: A few days ago, while testing a Gem with Ruby 1.9.3-preview1 tests were not detected when using `t.pattern` so I changed it to FileList:

    t.test_files = FileList['specs/**/*_spec.rb']

  10. Johnathon says:

    I like the simplicity of MiniTest, but even though it's default with Ruby 1.9, I find the lack of Rails support odd. Of course, there are things like minitest-rails , but even then, it's missing simple integration/acceptance test support. Capybara itself even seems to be biased toward Rspec.

    So I think MiniTest really needs better support and documentation (this is the main case since it's really hard to find anyone using it over Rspec) to use with projects (esp. Rails) before it'd be an contender to overthrowing Rspec.

  11. Richard says:

    Just putting it through its paces now. It reminds me a lot of an old gem called dust which added some basic pretty syntax on top of Test::Unit

    I like it when your tests are just method calls rather than method definitions. It makes it easy to build up tests from array data and still keep things nice and literal. An old example I wrote of making lots of tests out of array data or fixtures its for reference, but sometimes tests like that are very handy to do in certain circumstances.

    Now to just go through the Mocking toolkit. I am liking the idea of a test framework that has everything in it.

  12. Peter Aronoff says:

    @Marc For reporters under minitest, see minitest-reporters.

Other Posts to Enjoy

Twitter Mentions