Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ordinaryzelig/5266233 to your computer and use it in GitHub Desktop.
Save ordinaryzelig/5266233 to your computer and use it in GitHub Desktop.

(TLDR: http://railscasts.com/episodes/327-minitest-with-rails)

I just started a new Rails 3.1 project. I decided to use MiniTest::Spec for all my testing. I also wanted to use my typical testing tools: Capybara, Spork, etc. I didn't find much support for MiniTest, and even less for MiniTest::Spec. Here's some documentation on what I ended up with.

Pertinent gems I used at the time of this writing:

# Ruby 1.9.2-p290
gem 'rails', '3.1.0'
gem 'spork', '> 0.9.0.rc'
gem 'spork-testunit', '0.0.5'
gem 'factory_girl', '2.1.2'
gem 'capybara', '1.1.1'
gem 'capybara_minitest_spec', '0.2.1'
gem 'database_cleaner', '0.6.7'
gem 'minitest', '2.5.1'

There's a gem called minitest-rails, but all it does is provide generators and not much else. The files it generates for MiniTestSpec are not very spec-ish:

# Something along the lines of...
class ProjectTest < MiniTest::Rails::Model
  it 'passes' do
    true.must_be true
  end
end

What I want is something more like this:

describe Project do
  it 'passes' do
    true.must_be true
  end
end

It also doesn't provide integration tests. Let's just forget about generators for tests for now. You'll soon see that they're so simple that you can easily make a generator if you still really want them.

# config/application.rb
config.generators do |g|
  g.test_framework nil # Using minitest, but don't want generators.
end

Rails models should be easy to test since there's really no trick to it. So the example above with the pure spec-ish syntax should work out of the box.

Integration tests are where it gets tricky. I use Capybara for integration tests. Capybara makes it easy to roll with your own test suite. require 'capybara/rails' still applies to us since we are inside a Rails app. And then we just have to include Capybara::DSL to get methods like visit, fill_in, save_and_open_page, etc. But the trick for us is that we need to put these inside our integration test superclass. We also need the ability to call named routes like root_path and new_project_path. Usually, we would include them in ActionController::IntegrationTest, but we're not using Rails' built-in test suite. By default, when you call describe at the root level (like the project test above), you're declaring a subclass of MiniTest::Spec. But you can specify a matcher and a corresponding superclass to use if it matches the description. So here's what I ended up doing:

# test/test_helper.rb.
require 'minitest/autorun'
require 'capybara/rails'

# If description name ends with 'integration', use this RequestSpec class.
# It has all the integration test goodies.
class RequestSpec < MiniTest::Spec
  include Rails.application.routes.url_helpers
  include Capybara::DSL
end
MiniTest::Spec.register_spec_type /integration$/i, RequestSpec

Here's an example of an integration test:

# test/integration/projects_test.rb
require 'test_helper'

describe 'Project integration' do

  it 'is created by submitting a form' do
    visit new_project_path
    fill_in 'Title', with: 'Blog about this'
    click_button 'Save'
    project = Project.first
    within "#project_#{project.id}" do
      page.has_content?('Blog about this').must_be true
    end
  end

end

We can take it a step further. Capybara has excellent node matchers that work no matter what testing framework you're using.

page.has_css?('table.results')`)

But it also has built-in RSpec support for them too.

page.should have_css('table.results')`

That's closer to what we want for MiniTest::Spec. But MiniTest::Spec works much differently than RSpec. MiniTest::Spec is 'less-magical'. It uses more practical magic. I wrote a gem called capybara_minitest_spec that gives us just what we want. Now we can replace

page.has_content?('Blog about this').must_be true

with

page.must_have_content('Blog about this')

Much better.

With capybara_minitest_spec, we can also easily add custom matchers like so:

# test/support/custom_capybara_expectations.rb
class Capybara::Session
  def has_flash_message?(message)
    within '#flash' do
      has_content? message
    end
  end
end
CapybaraMiniTestSpec::Matcher.new(:has_flash_message?)

Now we can do this:

page.must_have_flash_message('Successfully created')
# and
page.wont_have_flash_message('There were errors')

After adding more and more custom matchers in this way, it may be better to clean it up a bit:

# test/support/custom_capybara_expectations.rb
module CustomCapybaraExpectations
  def has_flash_message?(message)
    within '#flash' do
      has_content? message
    end
  end
end
Capybara::Session.send :include, CustomCapybaraExpectations
CustomCapybaraExpectations.public_instance_methods(false).each do |name|
  CapybaraMiniTestSpec::Matcher.new(name)
end

Now, any public instance method we define in the CustomCapybaraExpectations module will be added as a new CapybaraMiniTestSpec::Matcher.

"What about functional tests", you ask. I find that integration tests cover 95% of what I need functional tests to cover, so I don't do a lot of functional testing. When it comes to things like authentication, I'll do functional testing. But in my current project which prompted this article, I haven't gotten that far yet.

Here is my final test_helper.rb:

# test/test_helper.rb
require 'spork'

Spork.prefork do

  # Environment.
  ENV["RAILS_ENV"] = "test"
  require File.expand_path('../../config/environment', __FILE__)

  # MiniTest and Capybara.
  require 'minitest/autorun'
  require 'capybara/rails'

  # Require ruby files in support dir.
  Dir[File.expand_path('test/support/*.rb')].each { |file| require file }

  # Database cleaner.
  DatabaseCleaner.strategy = :truncation
  class MiniTest::Spec
    before :each do
      DatabaseCleaner.clean
    end
  end

  # If description name ends with 'integration', use this RequestSpec class.
  # It has all the integration test goodies.
  class RequestSpec < MiniTest::Spec
    include Rails.application.routes.url_helpers
    include Capybara::DSL
    include IntegrationHelpers
  end
  MiniTest::Spec.register_spec_type /integration$/i, RequestSpec

end

Spork.each_run do

  FactoryGirl.reload
  Rails.application.reload_routes!

end

On a side note, I turned off the turn gem because it wasn't very informative in terms of showing me what line my test was failing on. I just commented it out of my Gemfile.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment