This is a quick guide to get up to speed with rspec in rails. This was made as a guide while building this repo for reference.
Other than some of the more obvious benefits of testing, when you look at a rspec test suite, you see how things are named and modeled. For example, a test for a user model might look something like this
# sample user_spec.rb
describe User do
it 'has a valid username' do
expect(build(:user)).to be_valid
end
it 'is invalid without a username' do
expect(build(:user, username: nil)).to_not be_valid
end
end
If you read it, it looks like you're testing for User has a valid username
, User is invalid without a username
. This is the benefit of
using rspec instead of Test::Case
as it gives you human-like scenarios to test.
For the sample repo, here is the relevant testing gems I am using with a breakdown of each gem
# Gemfile
group :development, :test do
gem 'byebug'
gem 'web-console', '~> 2.0'
gem 'spring'
gem 'rspec-rails', '~> 3.0' # rspec rails library
gem 'shoulda-matchers'
gem 'factory_girl_rails'
gem 'faker'
gem 'capybara'
gem 'selenium-webdriver'
gem 'database_cleaner'
end
let's focus on lines 35-40
- 35:
Shoulda Matchers
provides RSpec- and Minitest-compatible one-liners that test common Rails functionality. - 36:
factory_girl
is a fixtures replacement with a straightforward definition syntax, support for multiple build strategies and allows us to use this sample data for tests in our specs - 37:
faker
provides fake data so we dont have to write it - 38:
capybara
helps test web applications by simulating how a real user would interact with your app. This is especially important for feature tests - 39:
selenium-webdriver
allows for web browser automation. it is also good when running js in your specs - 40:
database_cleaner
cleans our data (after some configuration) after each run, so we dont get duplicated (which could fail certain validations)
after running bundle install
, we then run rails generate rspec:install
which generates our spec folder, and an .rspec
file
the next piece is adjusting our rails_helper
file which is what is configuring our rspec. Here's what mine looks like
# spec/rails_helper.rb
....
require 'capybara/rails'
config.mock_with :rspec
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each, :js => true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
this basically just sets up our dependencies from our gemfile, and lists capybara and database_cleaner.
The next part we should do is our factory set up, which will allow us to define what a sample object will look like, so we can reuse it
when testing the rest of our app. Let's set up a user factory. In our spec
folder, let's make a factories
directory.
# spec/factories/user.rb
FactoryGirl.define do
factory :user do
username { Faker::Internet.user_name }
email { Faker::Internet.email }
password { Faker::Internet.password(8) }
end
end
In this, we took the attributes that are defined in our user, and plugged faker
data in (but could be a simple string). This gives us
a user factory which we can reuse. While I'm here, I might as well make my question and answer factories too
# spec/factories/question.rb
FactoryGirl.define do
factory :question do
title { Faker::Company.catch_phrase }
body { Faker::Company.bs }
end
end
# spec/factories/answer.rb
FactoryGirl.define do
factory :answer do
body { Faker::Company.bs }
end
end
while we made our factories, let's test our model validations. In my 3 models, I added the following
# user.rb
class User < ActiveRecord::Base
has_secure_password
has_many :questions
has_many :answers
validates :username, presence: true
validates :email, presence: true
end
# question.rb
class Question < ActiveRecord::Base
belongs_to :user
has_many :answers
validates_presence_of :title
validates_presence_of :body
end
# answer.rb
class Answer < ActiveRecord::Base
belongs_to :user
belongs_to :question
validates_presence_of :body
end
now let's add our model specs, which are testing our validations
# spec/models/user_spec.rb
require 'rails_helper'
describe User do
it 'has a valid username' do
expect(build(:user)).to be_valid
end
it 'is invalid without a username' do
expect(build(:user, username: nil)).to_not be_valid
end
it 'is invalid without an email' do
expect(build(:user, email: nil)).to_not be_valid
end
it 'is invalid without matching passwords' do
expect(build(:user, password: 'password', password_confirmation: 'password1')).to_not be_valid
end
end
# spec/models/question_spec.rb
require 'rails_helper'
describe Question do
it 'has a valid title' do
expect(build(:question)).to be_valid
end
it 'is invalid without a title' do
expect(build(:question, title: nil)).to_not be_valid
end
it 'is invalid without a body' do
expect(build(:question, body: nil)).to_not be_valid
end
end
# spec/models/answer_spec.rb
require 'rails_helper'
describe Answer do
it 'should be valid' do
expect(build(:answer)).to be_valid
end
it 'should not be valid without a body' do
expect(build(:answer, body: nil)).to_not be_valid
end
end
now if we run rspec spec/models/
in our terminal we should see
Finished in 0.74634 seconds (files took 2.25 seconds to load)
9 examples, 0 failures
Now that our models are working, let's test our controllers. A major feature of this app (and most in general) is sessions. Let's start with testing our sessions, and the functionality that comes with that. Let's start by writing our tests first
# spec/controller/sessions_controller_spec.rb
describe SessionsController do
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 the root url' do
user = create(:user)
post :create, session: { email: user.email, password: user.password }
expect(controller.session[:user_id]).to eq @current_user
expect(response).to have_http_status(200)
end
end
end
end
here we are testing that when a user logs in with an invalid password that it will render a new signin form, and when a user signs in that we are expecting the status code to be a 200, which means when sending a post request that the user was created and is currently a current_user
here is the code for this test
# controller/sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by_email(params[:email])
# If the user exists AND the password entered is correct.
if user && user.authenticate(params[:password])
# Save the user id inside the browser cookie. This is how we keep the user
# logged in when they navigate around our website.
session[:user_id] = user.id
redirect_to root_path
else
# If user's login doesn't work, send them back to the login form.
render :new
flash.notice = 'Email and password do not match'
end
end
def destroy
session[:user_id] = nil
redirect_to '/login'
end
end
and if we go back in the terminal and run rspec spec/controllers/sessions_controller_spec.rb
we see
Finished in 0.47184 seconds (files took 2.27 seconds to load)
2 examples, 0 failures
the rest of the controller specs are testing for CRUD operations on both of the models. If you would like to see the specs they are here
The last part of this is testing our features. Other than sessions and auth which were tested above in our sessions controller and models, a feature of this app is to be able to upvote posts. Let's test for that.
For the model and controller specs, we were testing certain actions. We we're testing that things were being rendered, we are being redirected things are or are not valid, etc.
For the feature tests, we do something somewhat different. We are now testing scenarios that our users will see or interact with, and will be relying on a mix of rspec and capybara.
Although not complete, here is my feature spec for clicking the 'upvote' link in the index page
# spec/features/user_upvotes_question_spec.rb
require 'rails_helper'
feature 'User can upvote question' do
scenario 'they see questions on the page' do
visit root_path
expect(page).to have_content 'Example Q & A'
end
scenario 'they upvote a specific question' do
visit root_path
click_link("upvote")
expect(page).to redirect_to '/'
end
end
I hope this served as a good guide to get started with. My only 'hesitation' with testing is so much of it is out of date, or can be in a different format, that it can be hard to get going with. I have found that the docs are solid, but even then they do not always have a correct answer.
links I liked