(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
endWhat I want is something more like this:
describe Project do
it 'passes' do
true.must_be true
end
endIt 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.
endRails 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, RequestSpecHere'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
endWe 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 truewith
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)
endNow, 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!
endOn 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.