Skip to main content

Rails 3.2, MiniTest Spec and Capybara

What do you do when you love your spec testing with Capybara but you want to veer off the beaten path of Rspec and forge ahead into MiniTest waters? Follow along, and you'll have not one, but two working solutions.

The setup

Quickly now, let's throw together an app to test this out. I'm on rails 3.2.9.

$ rails new minicap

Edit the Gemfile to include a test and development block

group :development, :test do
  gem 'capybara'
  gem 'database_cleaner'
end

Note the inclusion of database_cleaner as per the capybara documentation

And bundle:

$ bundle

We will, of course, need something to test against, so for the sake of it, lets throw together a scaffold, migrate our database and prepare our test database all in one big lump. If you are unclear on any of this, go read the guides.

$ rails g scaffold Book name:string author:string
$ rake db:migrate
$ rake db:test:prepare

Make it minitest

To make rails use minitest, we simply add a require statement to our test_helper.rb:

require 'minitest/autorun'
require 'capybara/rails'

We can add in a quick integration test to verify that everything is working correctly up to this point:

$ touch test/integration/book_spec.rb

If you are that way inclined, you could create an "acceptance" directory and pop your tests in there instead. I'm not too bothered by the naming right now, because this is a proof of concept.

Now edit the book_spec.rb file we created

require 'test_helper'

describe Book do
  it "creates a new book" do
    1.must_equal 1
  end
end

Running this passes nicely:

$ ruby -Itest test/integration/book_spec.rb

Setup is complete. We have a nice little project with which to experiment.

Failing to use Capybara

At this point we can try to throw in some navigation from the Capybara DSL to see if it will work. Update book_spec to include "visit":

describe Book do
  it "creates a new book" do
    visit('/books/new')
    1.must_equal 1
  end
end

and you will get an error when you run the test again - something to the effect of

$ NoMethodError: undefined method 'visit' for #<#:0x43c20f0>

Fix what you break

The rest of this post will focus on getting this small piece of code to work. I've found two ways so far. I'll present both and you can decide which you prefer.

The way of the Severed Rodent Hand

Pinched pretty much verbatim from the excellent rstat.us project, the Way of the Severed Rodent Hand uses a module. Working from the principle of the simplest thing that works, I'm going to start out in book_spec.rb and do this:

describe Book do
  Include Capybara::DSL
  it "creates a new book" do
    visit('/books/new')
    1.must_equal 1
  end
end

This produces a passing test. Dig a little deeper though and we will notice that the Capybara documentation suggests using a teardown method to instruct DatabaseCleaner to clean our database as well as reset sessions. MiniTest supports teardown hooks, but we will use and after method to start with:

describe Book do
  Include Capybara::DSL
  after do
    DatabaseCleaner.clean       # Truncate the database
    Capybara.reset_sessions!    # Forget the (simulated) browser state
    Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver
  end

  it "creates a new book" do
    visit('/books/new')
    1.must_equal 1
  end
end

Our tests work - and everything is right with the world.

Sever

Of course, no self respecting rubyist is going to leave this code as it stands. As soon as we add another resource, we will be duplicating the after block. Instead, we are going to sever that code and create a module, just like rstat.us. I'm going to put this into my test_helper file for simplicity but on a larger project, you might prefer to use a dedicated integration_helper or acceptance_helper:

module IntegrationTestHelper
  include Capybara::DSL
  
  def teardown
    DatabaseCleaner.clean       # Truncate the database
    Capybara.reset_sessions!    # Forget the (simulated) browser state
    Capybara.use_default_driver # Revert Capybara.current_driver to Capybara.default_driver
  end
end

I've also taken the opportunity to move the code from "after" into a teardown hook - MiniTest will pick this up for us because it is just awesome! To complete this refactor, we must include our module into our spec. Open up book_spec again and change it a little:

describe Book do
  include IntegrationTestHelper

  it "creates a new book" do
    puts 'create'
    visit('/books/new')
    1.must_equal 1
  end
end

Our tests still pass. We are golden.

The Classical method

Using a module as we just did works well. Nothing to fix there. However there's a trade-off, we have that little "include IntegrationTestHelper" in all our test that need access to Capybara's DSL. We can swap out this inconvenience for a slightly different one - this one based on classes instead of modules. It has it's own inconvenience, but you pay's your money, you takes your choice.

Set up a class

This method relies on a class in our test_helper - let's code it up like this:

class IntegrationHelper < MiniTest::Spec
  include Capybara::DSL
 
  after do
    DatabaseCleaner.clean
    Capybara.reset_sessions!
    Capybara.use_default_driver
  end
end
MiniTest::Spec.register_spec_type( /Integration$/, IntegrationHelper )

The magic here is the last line - this basically says that for any test that is described as "Integration", set the parent to IntegrationHelper (and thus inherit all the goodness of the DSL and the after block). Of course this magic can only be hooked up if we change book_spec to include the word Integration in our initial describe:

describe 'Book Integration' do
  #include IntegrationTestHelper

  it "creates a new book" do
    visit('/books/new')
    1.must_equal 1
  end
end

This passes. Changing your register_spec_type regex to something like "/Integration$/i" and you will have a case insensitive match if you prefer to use normal english casing on subsequent words.

Wrap up

Two methods, both work. On the one hand, you have to "include", on the other hand you have to describe correctly but you get a class hierarchy. I know religious wars have been started over less, but I encourage you to try both and go with your gut. If you have a better/another way, drop me a comment and share the love.

Comments

  1. I'm am trying to use this, but no matter which method I use, I'm still getting errors. I'm following the railstutorial and trying to test my users with

    visit user_path(user)

    I keep getting the following error when I try to run the test:

    undefined method user_path

    Any chance you know what's going wrong?

    ReplyDelete
  2. You need to include the url helpers - in your IntegrationHelper, add the following:

    include Rails.application.routes.url_helpers

    and you should be good to go

    ReplyDelete
  3. You've got a little error. It should be 'rails new minicap'

    ReplyDelete
  4. Thanks raul - I've updated the post and fixed it :)

    ReplyDelete

Post a Comment

Popular posts from this blog

Getting started with Ruby on Rails 3.2 and MiniTest - a Tutorial

For fun, I thought I would start a new Ruby on Rails project and use MiniTest instead of Test::Unit. Why? Well MiniTest is Ruby 1.9s testing framework dejour, and I suspect we will see more and more new projects adopt it. It has a built in mocking framework and RSpec like contextual syntax. You can probably get away with fewer gems in your Gemfile because of that. Getting started is always the hardest part - let's jump in with a new rails project rails new tddforme --skip-test-unit Standard stuff. MiniTest sits nicely next to Test::Unit, so you can leave it in if you prefer. I've left it out just to keep things neat and tidy for now. Now we update the old Gemfile: group :development, :test do gem "minitest" end and of course, bundle it all up.....from the command line: $ bundle Note that if you start experiencing strange errors when we get in to the generators later on, make sure you read about rails not finding a JavaScript runtime . Fire up...

Getting started with Docker

Docker, in the beginning, can be overwhelming. Tutorials often focus on creating a complex interaction between Dockerfiles, docker-compose, entrypoint scripts and networking. It can take hours to bring up a simple Rails application in Docker and I found that put me off the first few times I tried to play with it. I think a rapid feedback loop is essential for playing with a piece of technology. If you've never used Docker before, then this is the perfect post for you. I'll start you off on your docker journey and with a few simple commands, you'll be in a Docker container, running ruby interactively. You'll need to install Docker. On a Mac, I prefer to install Docker Desktop through homebrew: brew cask install docker If you're running Linux or Windows, read the official docs for install instructions. On your Mac, you should now have a Docker icon in your menu bar. Click on it and make sure it says "Docker desktop is running". Now open a terminal and ty...