Adding Minitest Spec in Rails 4

Rails 4 is out, and among its many improvements is upgrading the default testing library from Test::Unit to Minitest. And although Minitest has some surprisingly interesting features, the most discussed addition is its spec DSL. It is designed as a subset of RSpec’s DSL, though I’ll leave to others any direct comparisons to RSpec. Suffice it to say it its focus is to give you a friendly syntax to generate the test classes, methods, and assertions you’d normally write in plain Ruby.

It’ll take a little configuration, and yes, there’s a gem for that, but the DIY approach takes surprisingly little elbow grease and will teach you a couple of cool Minitest features. Let’s dive in!

Step 1: Setting the Minitest Dependency

Rails 4 sets the dependency on Minitest to “~> 4.2”. This means that it will use any Minitest 4.x release that is 4.2 or above. This also means that we can’t use the newly released Minitest 5, or the older 4.1. Since we want the spec DSL, we need to set the dependency to “~> 4.7”. To do that, let’s set the dependency in the Gemfile:

group :test do gem "minitest" , "~> 4.7" end

Step 2: Extending MiniTest::Spec::DSL

Minitest 4.7 introduced the MiniTest::Spec::DSL module. To add the spec DSL to our Rails tests, we’ll add this to the test/test_helper.rb file.

Let’s require the source file just after the rails/test_help require:

ENV [ "RAILS_ENV" ] ||= "test" require File . expand_path ( '../../config/environment' , __FILE__ ) require 'rails/test_help' require "minitest/spec"

The second change is to extend MiniTest::Spec::DSL in ActiveSupport::TestClass. Luckily for us, there is already a place in the helper for us to make these changes:

class ActiveSupport :: TestCase ActiveRecord :: Migration . check_pending! # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting fixtures :all # Add more helper methods to be used by all tests here... extend MiniTest :: Spec :: DSL end

Cool Minitest trick #1: register_spec_type

The last change is to tell MiniTest::Spec to use ActiveSupport::TestCase when describing an ActiveRecord model. We do this by calling Minitest’s register_spec_type method.

class ActiveSupport :: TestCase ActiveRecord :: Migration . check_pending! # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting fixtures :all # Add more helper methods to be used by all tests here... extend MiniTest :: Spec :: DSL register_spec_type self do | desc | desc < ActiveRecord :: Base if desc . is_a? Class end end

Step 3: Writing Specs

Now that we’ve configured the spec DSL, let’s use it! Let’s assume we have the following test in test/models/user_test.rb :

require "test_helper" class UserTest < ActiveSupport :: TestCase def valid_params { name: "John Doe" , email: "john@example.com" } end def test_valid user = User . new valid_params assert user . valid? , "Can't create with valid params: #{ user . errors . messages } " end def test_invalid_without_email params = valid_params . clone params . delete :email user = User . new params refute user . valid? , "Can't be valid without email" assert user . errors [ :email ], "Missing error when without email" end end

We can convert this test to the spec DSL one section at a time. Let’s start with replacing the class with a describe block:

require "test_helper" describe User do def valid_params { name: "John Doe" , email: "john@example.com" } end

We can bypass the need to explicitly define a class inheriting from ActiveSupport::TestCase because User inherits from ActiveRecord and we registered the spec type in the previous step.

Next we can replace the test methods with it blocks:

def valid_params { name: "John Doe" , email: "john@example.com" } end it "is valid with valid params" do user = User . new valid_params assert user . valid? , "Can't create with valid params: #{ user . errors . messages } " end it "is invalid without an email" do params = valid_params . clone params . delete :email user = User . new params refute user . valid? , "Can't be valid without email" assert user . errors [ :email ], "Missing error when without email" end

Now let’s replace the calls to the assertions with Minitest’s expectations. In the first test block, we are passing user.valid? to assert . The spec DSL provides many assertions as expectations, and in this case we can write this test using the must_be expectation. That would look like this:

it "is valid with valid params" do user = User . new valid_params user . must_be :valid? # Must create with valid params end

In the next test block, we are refuting that the user is valid. We can use the wont_be expectation for that. And then we are asserting that there are errors on the email attribute. We can use a combination of the must_be expectation and the present? method Rails adds to clean that up a bit:

it "is invalid without an email" do params = valid_params . clone params . delete :email user = User . new params user . wont_be :valid? #Must not be valid without email user . errors [ :email ]. must_be :present? # Must have error for missing email end

We can also move some helper methods to let blocks. In the end, here is what the test can look like using the spec DSL:

require "test_helper" describe User do let ( :user_params ) { { name: "John Doe" , email: "john@example.com" } } let ( :user ) { User . new user_params } it "is valid with valid params" do user . must_be :valid? # Must create with valid params end it "is invalid without an email" do # Delete email before user let is called user_params . delete :email user . wont_be :valid? # Must not be valid without email user . errors [ :email ]. must_be :present? # Must have error for missing email end end

Step 4: Smoothing The Rough Edges

The Minitest spec DSL does not support nested context blocks, but it does support nested describe blocks. Except… ActiveSupport::TestCase also defines a describe method, which stomps on the spec DSL. Oh no!

But wait, this is Ruby! To use nested describe blocks in your tests, we just need to remove the method from ActiveSupport::TestCase. To do this, add a call to remove_method just before MiniTest::Spec::DSL is added in the test helper:

class ActiveSupport :: TestCase ActiveRecord :: Migration . check_pending! # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting fixtures :all # Add more helper methods to be used by all tests here... class << self remove_method :describe end extend MiniTest :: Spec :: DSL register_spec_type self do | desc | desc < ActiveRecord :: Base if desc . is_a? Class end end

If you prefer expectations to assertions, we’ll need to add expectations for the several assertions that Rails provides, such as assert_response , assert_redirected_to , and assert_difference . We can do all this in the test helper file.

Cool Minitest trick #2: infect_an_assertion

First, create a new module and use the method infect_an_assertion that Minitest provides:

module MyApp::Expectations infect_an_assertion :assert_difference , :must_change infect_an_assertion :assert_no_difference , :wont_change end

Then we can include that module in Object so that those expectations are available everywhere:

class Object include MyApp :: Expectations end

Now we can use these expectations in our tests. Yay!

it "is able to be saved when valid" do lambda { user . save }. must_change "User.count" , + 1 end

That’s a Wrap!

As you can see, Minitest and Rails go hand in hand. I would even go so far as to say they are BFFs, like I did in this presentation. I hope you give Minitest a shot. Don’t let its size fool you. It may be small, but it’s a surprisingly powerful, full-featured testing library.

If this seems like too much configuration to manage on your own, that’s okay! Feel free to check out the minitest-rails gem, which does all this for you, includes some handy rake tasks, and lets you generate tests using the spec DSL. Either way, hopefully you know a little more about the test framework that comes with Rails 4.