(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.