logo-new mail facebook Dribble Social Icon Linkedin Social Icon Twitter Social Icon Github Social Icon Instagram Social Icon Arrow_element diagonal-decor rectangle-decor search arrow circle-flat
Capybara Splashing
Development
November 16, 2016

Setting Up RSpec And Capybara In Rails 5 For Testing

Michael Crismali Sr Software Consultant

There are a lot of great tools for testing Rails apps; RSpec and Capybara are particularly great. While basic integration of these two is fairly straightforward, there are a few gotchas that can lead to some fairly confusing behavior.

(If you’re more interested in unit testing the JavaScript in your Rails app, check out this post on exactly that.)

What Are The Tools?

RSpec is a hugely popular behavior driven development (BDD) oriented testing framework in the Ruby community.

Capybara makes it easy to interact with out application the way our users ultimately do: Through the browser. It has a readable DSL and a variety of configuration options (we’ll get into those later).

Getting the tools

Obviously you’ll want to install RSpec and Capybara, but you’ll also need Selenium and Database Cleaner to help hold things together.

Add them to the :test and :development groups in your Gemfile and then bundle install:

group :development, :test do
  gem "database_cleaner"
  gem "rspec-rails"
end

group :test do
  gem "capybara"
  gem "selenium-webdriver"
end

Installing and Configuring RSpec

You’ll need to run rails generate rspec:install which will make a .rspec, spec/spec_helper.rb, spec/rails_helper.rb files and they’ll contain either sensible defaults and tons comments.

spec_helper.rb

After uncommenting some configuration options and removing most of the comments, we’re left with a pretty small amount of configuration:

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end

  config.shared_context_metadata_behavior = :apply_to_host_groups
  config.filter_run_when_matching :focus
  config.example_status_persistence_file_path = "spec/examples.txt"
  config.disable_monkey_patching!
  config.default_formatter = 'doc' if config.files_to_run.one?
  config.order = :random
  Kernel.srand config.seed
  # config.profile_examples = 10
end

I strongly recommend reading through all of those comments to get a good understanding of what each option does. Here though I’d like to call out a few that are particularly important (Note: These options aren’t critical for the rest of the post.).

mocks.verify_partial_doubles = true prevents you from stubbing any methods that don’t already exist on an object. In other words, it helps typo-proof your mocks.

config.disable_monkey_patching! prevents RSpec from monkey patching should and stub onto pretty much everything. This makes your objects behave in your tests as they would in “real” life.

config.order = :random and Kernel.srand config.seed make sure that the tests are run in a random order each time they’re run. This helps keep each test independent of one another by making sure they can’t accidentally depend on each other’s side effects.

I’ve also left config.profile_examples commented out because while it is really helpful to have it run to identify tests that are slowing your suite down, the additional console output from it can get a bit noisy.

rails_helper.rb

Once again after uncommenting some configuration options and removing most of the comments, we’re left with a pretty small amount of configuration:

ENV["RAILS_ENV"] ||= "test"
require File.expand_path("../../config/environment", __FILE__)
abort("The Rails environment is running in production mode!") if Rails.env.production?
require "spec_helper"
require "rspec/rails"
# Add additional requires below this line. Rails is not loaded until this point!

# Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

ActiveRecord::Migration.maintain_test_schema!

RSpec.configure do |config|
  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.use_transactional_fixtures = true
  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!
end

The only default to change is config.use_transactional_fixtures from true to false. This stops RSpec Rails from wrapping our tests in database transactions which means that, for the moment, records created for one test will be visible to the next test. Don’t worry though, you’ll have Database Cleaner manage our transactions soon. First though, we’ll set up Capybara.

Setting Up Capybara

Capybara is dead simple to add to RSpec. Just add require "capybara/rspec" to the list of requires in your rails_helper.rb.

By default Capybara uses the RackTest driver for testing your app. It’s incredibly fast, but it doesn’t run your JavaScript, so if you want your feature tests to make sure your JavaScript is working you’ll need to mark the tests by passing js: true to describe:

RSpec.describe "Signing in", js: true do
  # tests that actually execute JavaScript
end

Selenium is the default driver for JavaScript tests, which is why we included the selenium-webdriver gem. By default Selenium will use Firefox, but certain versions don’t play well with Selenium. If you’d like to use Chrome you can override it by placing the following in your rails_helper.rb:

Capybara.register_driver :selenium_chrome do |app|
  Capybara::Selenium::Driver.new(app, browser: :chrome)
end

Capybara.javascript_driver = :selenium_chrome

Note: To use Chrome you’ll need to have ChromeDriver installed, which if you have Homebrew installed is just brew install chromedriver.

Why Database Cleaner?

The following set up may seem a little odd at first, but it’s to combat a specific and rather hard to diagnose problem: Capybara tests that are js: true struggle with transactions. More specifically, Capybara spins up an instance of our Rails app that can’t see our test data transaction so even tho we’ve created a user in our tests, signing in will fail because to the Capybara run instance of our app, there are no users. Too oversimplify just a bit, it means that Capybara won’t be able to “see” our test database records.

One solution would be to abandon transactions for your js: true tests, but that alone is not sufficient. Your js: true tests will depend on a common set up and will have to be run in a particular order, coupling them and making issues incredibly difficult to debug.

A better way to go is to wrap most of our tests in transactions and use truncation for our js: true tests. Database Cleaner makes this really easy.

Integrating Database Cleaner

First, start by making sure our tests start with a clean slate by adding the following to our RSpec.configure block:

config.before(:suite) do
  DatabaseCleaner.clean_with(:truncation)
end

Now we need to tell Database Cleaner how to manage the database for regular tests and Selenium tests:

config.before(:each) do
  DatabaseCleaner.strategy = :transaction
end

config.before(:each, js: true) do
  DatabaseCleaner.strategy = :truncation
end

The order of these blocks is very significant. Right now our js: true block overrides the plain one when a spec is js: true. If it were reversed, Database Cleaner would always be using transactions and since our js: true tests can’t see those records there would be problems. Next we have to actually clean up the database:

# This block must be here, do not combine with the other `before(:each)` block.
# This makes it so Capybara can see the database.
config.before(:each) do
  DatabaseCleaner.start
end

config.after(:each) do
  DatabaseCleaner.clean
end

You may want to combine the before(:each) we just added with the previous one but don’t. If you combine them, Database Cleaner will open a transaction before being set to use truncation, which means Capybara won’t be able to “see” the test database records.

So now your rails_helper.rb should look something like:

ENV["RAILS_ENV"] ||= "test"
require File.expand_path("../../config/environment", __FILE__)
abort("The Rails environment is running in production mode!") if Rails.env.production?
require "spec_helper"
require "rspec/rails"
# Add additional requires below this line. Rails is not loaded until this point!
require "capybara/rspec"

# Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

ActiveRecord::Migration.maintain_test_schema!

Capybara.register_driver :selenium_chrome do |app|
 Capybara::Selenium::Driver.new(app, browser: :chrome)
end

Capybara.javascript_driver = :selenium_chrome
RSpec.configure do |config|
  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.use_transactional_fixtures = false
  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, js: true) do
    DatabaseCleaner.strategy = :truncation
  end

  # This block must be here, do not combine with the other `before(:each)` block.
  # This makes it so Capybara can see the database.
  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

Again, make sure your config.before(:each) blocks (whether they’re js: true or not) are in the order above.

Now when you run your tests you won’t have any shared database state and Capybara will be able to “see” everything it needs to be able to “see”. Plus you can run them in random order with no problems.


Photo credit: Bernard DUPONT. License. Original uncropped.

Tandem – software development companies in Chicago with practice areas in digital strategy, human-centered design, UI/UX, and web application and custom mobile development.

Let’s do something great together

We do our best work in close collaboration with our clients. Let’s find some time for you to chat with a member of our team.

Say Hi