GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

How We Test Rails Applications

I'm frequently asked what it takes to begin testing Rails applications. The hardest part of being a beginner is that you often don't know the terminology or what questions you should be asking. What follows is a high-level overview of the tools we use, why we use them, and some tips to keep in mind as you are starting out.

RSpec

We use RSpec over Test::Unit because the syntax encourages human readable tests. While you could spend days arguing over what testing framework to use, and they all have their merits, the most important thing is that you are testing.

Feature specs

Feature specs, a kind of acceptance test, are high-level tests that walk through your entire application ensuring that each of the components work together. They're written from the perspective of a user clicking around the application and filling in forms. We use RSpec and Capybara, which allow you to write tests that can interact with the web page in this manner.

Here is an example RSpec feature test:

# spec/features/user_creates_a_foobar_spec.rb

feature 'User creates a foobar' do
  scenario 'they see the foobar on the page' do
    visit new_foobar_path

    fill_in 'Name', with: 'My foobar'
    click_button 'Create Foobar'

    expect(page).to have_css '.foobar-name', 'My foobar'
  end
end

This test emulates a user visiting the new foobar form, filling it in, and clicking "Create". The test then asserts that the page has the text of the created foobar where it expects it to be.

While these are great for testing high level functionality, keep in mind that feature specs are slow to run. Instead of testing every possible path through your application with Capybara, leave testing edge cases up to your model, view, and controller specs.

I tend to get questions about distinguishing between RSpec and Capybara methods. Capybara methods are the ones that are actually interacting with the page, i.e. clicks, form interaction, or finding elements on the page. Check out the docs for more info on Capybara's finders, matchers, and actions.

Model specs

Model specs are similar to unit tests in that they are used to test smaller parts of the system, such as classes or methods. Sometimes they interact with the database, too. They should be fast and handle edge cases for the system under test.

In RSpec, they look something like this:

# spec/models/user_spec.rb

# Prefix class methods with a '.'
describe User, '.active' do
  it 'returns only active users' do
    # setup
    active_user = create(:user, active: true)
    non_active_user = create(:user, active: false)

    # exercise
    result = User.active

    # verify
    expect(result).to eq [active_user]

    # teardown is handled for you by RSpec
  end
end

# Prefix instance methods with a '#'
describe User, '#name' do
  it 'returns the concatenated first and last name' do
    # setup
    user = build(:user, first_name: 'Josh', last_name: 'Steiner')

    # excercise and verify
    expect(user.name).to eq 'Josh Steiner'
  end
end

To maintain readability, be sure you are writing Four Phase Tests.

Controller specs

When testing multiple paths through a controller is necessary, we favor using controller specs over feature specs, as they are faster to run and often easier to write.

A good use case is for testing authentication:

# spec/controllers/sessions_controller_spec.rb

describe 'POST #create' do
  context 'when password is invalid' do
    it 'renders the page with error' do
      user = create(:user)

      post :create, session: { email: user.email, password: 'invalid' }

      expect(response).to render_template(:new)
      expect(flash[:notice]).to match(/^Email and password do not match/)
    end
  end

  context 'when password is valid' do
    it 'sets the user in the session and redirects them to their dashboard' do
      user = create(:user)

      post :create, session: { email: user.email, password: user.password }

      expect(response).to redirect_to '/dashboard'
      expect(controller.current_user).to eq user
    end
  end
end

View specs

View specs are great for testing the conditional display of information in your templates. A lot of developers forget about these tests and use feature specs instead, then wonder why they have a long running test suite. While you can cover each view conditional with a feature spec, I prefer to use view specs like the following:

# spec/views/products/_product.html.erb_spec.rb

describe 'products/_product.html.erb' do
  context 'when the product has a url' do
    it 'displays the url' do
      assign(:product, build(:product, url: 'http://example.com')

      render

      expect(rendered).to have_link 'Product', href: 'http://example.com'
    end
  end

  context 'when the product url is nil' do
    it "displays 'None'" do
      assign(:product, build(:product, url: nil)

      render

      expect(rendered).to have_content 'None'
    end
  end
end

FactoryGirl

While writing your tests you will need a way to set up database records in a way to test against them in different scenarios. You could use the built-in User.create, but that gets tedious when you have many validations on your model. With User.create you have to specify attributes to fulfill the validations, even if your test has nothing to do with those validations. On top of that, if you ever change your validations later, you have to reflect those changes across every test in your suite. The solution is to use either factories or fixtures to create models.

We prefer factories (with FactoryGirl) over Rails fixtures, because fixtures are a form of Mystery Guest. Fixtures make it hard to see cause and effect, because part of the logic is defined in a file far away from the context in which you are using it. Because fixtures are implemented so far away from your tests, they tend to be hard to control.

Factories, on the other hand, put the logic right in the test. They make it easy to see what is happening at a glance and are more flexible to different scenarios you may want to set up. While factories are slower than fixtures, we think the benefits in flexibility and readability outweigh the costs.

Persisting to the database slows down tests. Whenever possible, favor using FactoryGirl's build_stubbed over create. build_stubbed will generate the object in memory and save you from having to write to the disk. If you are testing something in which you have to query for the object (like User.where(admin: true)), your database will be expecting to find it in the database, meaning you must use create.

Running specs with JavaScript

You will eventually run into a scenario where you need to test some functionality that depends on a piece of JavaScript. Running your specs with the default driver will not run any JavaScript on the page.

You need two things to run a feature spec with JavaScript.

  1. Install a JavaScript driver

    There are two types of JavaScript drivers. Something like Selenium will open a GUI browser and click around your page while you watch it. This can be a useful tool to visualize while debugging. Unfortunately, booting up an entire GUI browser is slow. For this reason, we prefer using a headless browser. For Rails, you will want to use either Poltergeist or Capybara Webkit.

  2. Tell the specific test to run with the JavaScript metadata key

     feature 'User creates a foobar' do
       scenario 'they see the foobar on the page', js: true do
         ...
       end
     end
    

With the following in place, RSpec will run any JavaScript necessary.

Database Cleaner

When running your tests by default, Rails wraps each scenario in a database transaction. This means, at the end of each test, Rails will rollback any changes to the database that happened within that spec. This is a good thing, as we don't want any of our tests having side effects on other tests.

Unfortunately, when we use a JavaScript driver, the test is run in another thread. This means it does not share a connection to the database and your test will have to commit the transactions in order for the running application to see the data. To get around this, we can allow the database to commit the data and subsequently truncate the database after each spec. This is slower than transactions, however, so we want to use truncation only when necessary.

This is where Database Cleaner comes in. Database Cleaner allows you to configure when each strategy is used. I recommend reading Avdi's post for all the gory details. It's a pretty painless setup, and I typically copy this file from project to project, or use Suspenders so that it's set up out of the box.

Test doubles and stubs

Test doubles are simple objects that emulate another object in your system. Often, you will want a simpler stand-in and only need to test one attribute, so it is not worth loading an entire ActiveRecord object.

car = double(:car)

When you use stubs, you are telling an object to respond to a given method in a known way. If we stub our double from before

car.stub(:max_speed).and_return(120)

we can now expect our car object to always return 120 when prompted for its max_speed. This is a great way to get an impromptu object that responds to a method without having to use a real object in your system that brings its dependencies with it. In this example, we stubbed a method on a double, but you can stub virtually any method on any object.

We can simplify this into one line:

car = double(:car, max_speed: 120)

Test spies

While testing your application, you are going to run into scenarios where you want to validate that an object receives a specific method. In order to follow Four Phase Test best practices, we use test spies so that our expectations fall into the verify stage of the test. Previously we used Bourne for this, but RSpec now includes this functionality in RSpec Mocks. Here's an example from the docs:

invitation = double('invitation', accept: true)

user.accept_invitation(invitation)

expect(invitation).to have_received(:accept)

Stubbing external requests with Webmock

Test suites that rely on third party services are slow, fail without an internet connection, and may have trouble with the services' rate limits or lack of a sandbox environment.

Ensure that your test suite does not interact with third party services by stubbing out external HTTP requests with Webmock. This can be configured in spec/spec_helper.rb:

require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)

Instead of making third party requests, learn how to stub external services in tests.

What's next?

This was just an overview of how to get started testing Rails. To expedite your learning, I highly encourage you to take our TDD workshop, where you cover these subjects in depth by building two Rails apps from the ground up. It covers refactoring both application and test code to ensure both are maintainable. Students of the TDD workshop also have access to office hours, where you can ask thoughtbot developers any questions you have in real time.

I took this class as an apprentice, and I can't recommend it enough.