Awesome
Rspec Best Practices
A collection of Rspec testing best practices
Table of Contents
- Describe your methods
- Use Context
- Only one expectation per example
- Test valid, edge and invalid cases
- Use let
- DRY
- Optimize database queries
- Use factories
- Choose matchers based on readability
- Run specific tests
- Debug Capybara tests with save_and_open_page
- Only enable JS in Capybara when necessary
- Consult the logs
- Other tips
- More Resources
- Libraries
Describe your methods
Keep clear the methods you are describing using "." as prefix for class methods and "#" as prefix for instance methods.
# wrong
describe "the authenticate method for User" do
describe "the save method for User" do
# correct
describe ".authenticate" do
describe "#save" do
Use context
Use context to organize and DRY up code, keep spec descriptions short, and improve test readability.
# wrong
describe User do
it "should save when name is not empty" do
User.new(:name => 'Alex').save.should == true
end
it "should not save when name is empty" do
User.new.save.should == false
end
it "should not be valid when name is empty" do
User.new.should_not be_valid
end
it "should be valid when name is not empty" do
User.new(:name => 'Alex').should be_valid
end
end
# correct
describe User do
let (:user) { User.new }
context "when name is empty" do
it "should not be valid" do
expect(user.valid?).to be_false
end
it "should not save" do
expect(user.save).to be_false
end
end
context "when name is not empty" do
let (:user) { User.new(:name => "Alex") }
it "should be valid" do
expect(user.valid?).to be_true
end
it "should save" do
expect(user.save).to be_true
end
end
end
Only one expectation per example
Each test example should make only one assertion. This helps you on find errors faster and makes your code easier to read and maintain.
# wrong
describe "#fill_gass" do
it "should have valid arguments" do
expect { car.fill_gas }.to raise_error(ArgumentError)
expect { car.fill_gas("foo") }.to_raise_error(TypeError)
end
end
# correct
describe "#fill_gass" do
it "should require one argument" do
expect { car.fill_gas }.to raise_error(ArgumentError)
end
it "should require a numeric argument" do
expect { car.fill_gas("foo") }.to_raise_error(TypeError)
end
end
Test valid, edge and invalid cases
This is called Boundary value analysis, it’s simple and it will help you to cover the most important cases. Just split-up method’s input or object’s attributes into valid and invalid partitions and test both of them and there boundaries. A method specification might look like that:
describe "#month_in_english(month_id)" do
context "when valid" do
it "should return 'January' for 1" # lower boundary
it "should return 'March' for 3"
it "should return 'December' for 12" # upper boundary
end
context "when invalid" do
it "should return nil for 0"
it "should return nil for 13"
end
end
Use let
When you have to assign a variable to test, instead of using a before each block, use let. It is memoized when used multiple times in one example, but not across examples.
# wrong
describe User, '#locate'
before(:each) { @user = User.locate }
it 'should return nil when not found' do
@user.should be_nil
end
end
# correct
describe User
let(:user) { User.locate }
it 'should have a name' do
user.name.should_not be_nil
end
end
DRY
Be sure to apply good code refactoring principles to your tests.
Use before and after hooks:
describe Thing do
before(:each) do
@thing = Thing.new
end
describe "initialized in before(:each)" do
it "has 0 widgets" do
@thing.should have(0).widgets
end
it "does not share state across examples" do
@thing.should have(0).widgets
end
end
end
Extract reusable code into helper methods:
# spec/features/user_signs_in_spec.rb
require 'spec_helper'
feature 'User can sign in' do
scenario 'as a user' do
sign_in
expect(page).to have_content "Your account"
end
end
# spec/features/user_signs_out_spec.rb
require 'spec_helper'
feature 'User can sign out' do
scenario 'as a user' do
sign_in
click_link "Logout"
expect(page).to have_content "Sign up"
end
end
# spec/support/authentication_helper.rb
module AuthenticationHelper
def sign_in
visit root_path
user = FactoryBot.create(:user)
fill_in 'user_session_email', with: user.email
fill_in 'user_session_password', with: user.password
click_button "Sign in"
return user
end
end
# spec/spec_helper.rb
RSpec.configure do |config|
config.include AuthenticationHelper, type: :feature
# ...
Optimize database queries
Large test suites can take a long time to run. Don't load or create more data than necessary.
describe User do
it 'should return top users in User.top method' do
@users = (1..3).collect { Factory(:user) }
top_users = User.top(2).all
top_users.should have(2).entries
end
end
class User < ActiveRecord::Base
named_scope :top, lambda { |*args| { :limit => (args.size > 0 ? args[0] : 10) } }
end
Use factories
Use factory_bot to reduce the verbosity when working with models.
# before
user = User.create( :name => "Genoveffa",
:surname => "Piccolina",
:city => "Billyville",
:birth => "17 Agoust 1982",
:active => true)
# after
user = Factory.create(:user)
Choose matchers based on readability
RSpec comes with a lot of useful matchers to help your specs read more like language. When you feel there is a cleaner way … there usually is!
Here are some examples, before and after they are applied:
# before: double negative
object.should_not be_nil
# after: without the double negative
object.should be
# before: 'lambda' is too low level
lambda { model.save! }.should raise_error(ActiveRecord::RecordNotFound)
# after: for a more natural expectation replace 'lambda' and 'should' with 'expect' and 'to'
expect { model.save! }.to raise_error(ActiveRecord::RecordNotFound)
# before: straight comparison
collection.size.should == 4
# after: a higher level size expectation
collection.should have(4).items
Run specific tests
Running your entire test suite over and over again is a waste of time.
Run example or block at specified line:
# in rails
rake spec SPEC=spec/models/demand_spec.rb:30
# not in rails
rspec spec/models/demand_spec.rb:30
Run examples that match a given string:
# in rails
rake spec SPEC=spec/controllers/sessions_controller_spec.rb \
SPEC_OPTS="-e \"should log in with cookie\""
# not in rails
rspec spec/login_spec.rb -e "should log in with cookie"
In Rails, run only your integration tests:
rake spec:features
Debug Capybara tests with save_and_open_page
Capybara has a save_and_open_page
method. As the name implies, it saves the page — complete with styling and images — and opens it in your browser so you can inspect it:
it 'should register successfully' do
visit registration_page
save_and_open_page
fill_in 'username', :with => 'abinoda'
end
Only enable JS in Capybara when necessary
Only enable JS when your tests require it. Enabling JS slows down your test suite.
# only use js => true when your tests depend on it
it 'should register successfully', :js => true do
visit registration_page
fill_in 'username', :with => 'abinoda'
end
Unless the pages you are testing require JS, it's best to disable JS after you're done writing the test so that the test suite runs faster.
Consult the logs
When you run any rails application (the webserver, tests or rake tasks), ouput is saved to a log file. There is a log file for each environment: log/development.log, log/test.log, etc.
Take a moment to open up one of these log files in your editor and take a look at its contents. To watch your test log files, use the *nix tool tail:
tail -f log/test.log
Curious what -f means? Check the man page for the tail utility: man tail
Other tips
- When something in your application goes wrong, write a test that reproduces the error and then correct it. You will gain several hour of sleep and more serenity.
- Use solutions like guard (using guard-rspec) to automatically run all of your test, without thinking about it. Combining it with growl, it will become one of your best friends. Examples of other solutions are test_notifier, watchr and autotest.
- Use TimeCop to mock and test methods that relies on time.
- Use Webmock to mock HTTP calls to remote service that could not be available all the time and that you want to personalize.
- Use a good looking formatter to check if your test passed or failed. I use fuubar, which to me looks perfect.
More Resources
- Why Our Code Smells
- Request Specs and Capybara
- How I Test
- Dmytro best practices
- Great Rails/Rspec example code