blowmage A Mike Moore joint

minitest-rails: Roadmap to 1.0

tl;dr: The newest minitest and minitest-rails are awesome.

I've been working on the minitest-rails project for almost two years now. That is a very long time to work on a project without having a stable release. Fortunately with the 0.9 releasethat I released earlier tonight we are on our way to stability.

The Problem

Inheritance is great, except when it isn't. All Rails tests inherit from ActiveSupport::TestCase. In Rails 3, ActiveSupport::TestCase inherits from Test::Unit::TestCase, which inherits from MiniTest::Unit::TestCase. (In Ruby 1.9 and higher. If you are on Ruby 1.8 you can install the minitesttushim gem to inject minitest into the inheritance chain.)

What we want, however, is to inherit from MiniTest::Spec. In fact, for almost a year during the development of Rails 4 ActiveSupport::TestCase inheritedd from MiniTest::Spec. Unfortunately, that was changed before the first beta gem was released and it now inherits directly from MiniTest::Unit::TestCase. (Which is disappointing but totally appropriate for the core team to do. It's their project, and they should do what they think is best, always.) So what do we do? How can we change the nature of the Rails tests? Time to roll up our sleeves and get hacky!

A Terrible Solution

One suboptimal solution is to change the ancestry of the ActiveSupport::TestCase and inject MiniTest::Spec. For minitest-rails this meant creating a MiniTest::Rails::TestCase that inherits from MiniTest::Spec and includes all the Rails testing modules. For minitest-spec-rails this meant similarly replacing Test::Unit::TestCase with an implementation that inherits from MiniTest::Spec.

This approach generally works, but you have to be very careful in how you load the libraries that are getting replaced. You can replace a constant in Ruby, but if another object is already inheriting from the replaced object it will continue to inherit from it even after the constant is replaced. Problems arise when other libraries affect how code is loaded, and upend your carefully planned hack. And ultimately that is inevitable. (Don't even get me started on what Rails 3's threadsafe! option does to how code is loaded.)

A Sustainable Solution

What we really need is to enable MiniTest::Spec functionality without altering the ancestry. But how? Instead of using Ruby meta-programming for evil, how about we use it for good? Imagine if this worked:

class ActiveSupport::TestCase
  extend MiniTest::Spec::DSL
end

With the latest release of minitest that's the case. Ryan Davis added the ability to enable the spec DSL on any minitest TestCase. This means that this is functionally equivalent:

class TestMyStuff < MiniTest::Spec
  it "works" do
    assert true
  end
end

class TestMyStuffAgain < MiniTest::Test::Unit
  extend MiniTest::Spec::DSL

  it "works" do
    assert true
  end
end

Because of how MiniTest::Spec was designed this was an easy change to make. I love working in code that Ryan produces because this is generally the case.

The Unlimited Possibilities

This change means that supporting minitest's spec DSL is suddenly trivial. Even for older Rails 2.x apps! Specifying the version of minitest in your Rails project's Gemfile and including the following in your test helper enables the DSL:

require "minitest/spec"
class ActiveSupport::TestCase
  extend MiniTest::Spec::DSL
end

Unfortunately, that doesn't do everything for you. If you want to use the full spec DSL, you need to configure which TestCase is used in your tests. To do that you need to register the TestCase with the spec DSL:

class ActiveSupport::TestCase
  # Use AS::TestCase for the base class when describing a model
  register_spec_type(self) do |desc|
    desc < ActiveRecord::Base if desc.is_a?(Class)
  end
end

class ActionController::TestCase
  # Use AC::TestCase for the base class when describing a controller
  register_spec_type(self) do |desc|
    Class === desc && desc < ActionController::Metal
  end
  register_spec_type(/Controller( ?Test)?\z/i, self)
end

And so on. But there is another piece. Controller/Helper/Mailer tests also attempt to deduce the test subject from the test name to create an instance in the test. This is a bit more involved, and currently minitest-rails uses MiniTest::Rails::Testing::ConstantLookup to resolve the constant from the test name.

There are other little incompatibilities that need to be addressed to enable the spec DSL in your Rails apps. Fortunately, all this and more is included with minitest-rails, which is the easiest way to use advanced minitest functionality within your Rails app. (End sales pitch.)

What this means is that instead of minitest-rails hacking how Rails loads code, it can just declare how we want our tests configured. This is a huge step forward.

Upgrade Path

If you are already using minitest-rails, thank you! Because of the various ways we've attempted to inject minitest into Rails there is some cruft that has accumulated along the way. So for the 0.9 release we've added a few deprecation warnings for things that will be removed in 1.0. If you have an existing app using minitest-rails please look at the Upgrade Guide and make those changes.