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