A MiniTest::Spec Tutorial: Elegant Spec-Style Testing That Comes With Ruby
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 ofassert_in_delta
obj.must_be_empty
- Fails unless obj.empty?obj.must_be_instance_of(klass)
- Fails unlessobj.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 equalitylambda {}.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
- Using MiniTest::Spec with Rails
- From Test::Unit & Shoulda To MiniTest::Spec & MiniShoulda
- MiniTest: Ruby 1.9's Test Framework
- MiniTest::Mock - the simple mocking system included in MiniTest.
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.
September 3, 2011 at 4:46 pm
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'
September 5, 2011 at 6:14 am
Cool, I'm going to convert the specs for my new gem to MiniTest :) I like how it supports both TU and spec style.
September 5, 2011 at 8:48 pm
I think you can leave the minitest/spec require out if you include minitest/autorun.
September 5, 2011 at 9:11 pm
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! :-)
September 5, 2011 at 9:12 pm
Oriol: I prefer your approach, I'll update the article soon.
September 8, 2011 at 1:38 pm
This video by Ryan Davis has a good overview of MiniTest.
http://confreaks.net/videos/618-cascadiaruby2011-size-doesn-t-matter
September 8, 2011 at 2:37 pm
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 :(
September 8, 2011 at 4:36 pm
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.)
September 8, 2011 at 6:14 pm
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']
September 20, 2011 at 5:28 pm
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.
September 29, 2011 at 7:12 pm
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.
November 7, 2011 at 5:40 pm
@Marc For reporters under minitest, see minitest-reporters.