MiniTest::Spec setup with Capybara in Rails 3.1
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 didnt' 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 (e.g. page.has_css?(‘table.results’)). But it also has built-in RSpec support for them too (e.g. 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.
