RSpec is a great tool to test your code and helps prevent bugs during development. It’s especially helpful after returning to a codebase that you’re not as familiar with anymore, or after a large refactoring where lots of pieces are being moved around. It’s a little more work up front, but the peace of mind it can provide is well worth it.
In this guide, I’ll walk you though how to setup RSpec with FactoryBot (formerly known as FactoryGirl) and make everything play nicely with Devise.
This project assumes you've already setup and configured your Rails 6.0
project. It also assumes you've setup Devise
as well.
Special thanks to Dave for fixing a depreciation warning in the Gemfile
file.
Special thanks to Thuy for catching a minor typo in the controller_macros.rb
file.
If you happen to find a typo, please let me know!
DEPRECATION WARNING: Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper
warningRails 6.0
Ruby 2.5.3
RSpec 4.0 (dev)
FactoryBot 5.0.2
Devise 4.7.1
Let’s begin.
RSpec is pretty straight-forward to setup, with great documentation on the RSpec GitHub page. However, due to Rails 6.0
, there are a few minor changes that differ from their current guide.
First, add the latest RSpec
gem to your Gemfile
:
Gemfile
group :development, :test do
# There may be other lines in this block already. Simply append the following after:
%w[rspec-core rspec-expectations rspec-mocks rspec-rails rspec-support].each do |lib|
gem lib, git: "https://github.com/rspec/#{lib}.git", branch: 'main' # Previously '4-0-dev' or '4-0-maintenance' branch
end
end
We need the latest 4.0 version of RSpec
(currently in development) for Rails 6.0
, otherwise we’ll receive the following error:
RSpec error without version 4.0
ActionView::Template::Error: wrong number of arguments (given 2, expected 1)
Once that’s been added, install the gems via bundle:
Terminal
bundle install
Then generate the boilerplate configuration files:
Terminal
rails generate rspec:install
This creates the following files:
.rspec
spec
spec/spec_helper.rb
spec/rails_helper.rb
Once installed, RSpec
has a nice hook-in to automatically generate tests when running rails generate
. In this next part, I'll be showing you how to create a test from scratch, but it's best to install RSpec early on and let it do most of the work for you.
I’ll be showing you how to create a test for a controller. The process is the same for other parts of your app.
Simply navigate to the /spec
directory and create a folder called /controllers
. It should look like this: /spec/controllers
.
Then in your new /controllers
folder, create a file called CONTROLLER-NAME_spec.rb
, where CONTROLLER-NAME
is the name of the controller you’d like to test. If we had a controller called articles_controller.rb
, we’d name this test articles_controller_spec.rb
.
Add the following to the newly created articles_controller_spec.rb
file:
spec/controllers/articles_controller_spec.rb
require 'rails_helper'
# Change this ArticlesController to your project
RSpec.describe ArticlesController, type: :controller do
# This should return the minimal set of attributes required to create a valid
# Article. As you add validations to Article, be sure to adjust the attributes here as well.
let(:valid_attributes) {
{ :title => "Test title!", :description => "This is a test description", :status => "draft" }
}
let(:valid_session) { {} }
describe "GET #index" do
it "returns a success response" do
Article.create! valid_attributes
get :index, params: {}, session: valid_session
expect(response).to be_successful # be_successful expects a HTTP Status code of 200
# expect(response).to have_http_status(302) # Expects a HTTP Status code of 302
end
end
end
The above example assumes that Articles#index
requires a valid Devise
login session to access. In other words, you have to be logged in to view it.
I won't get into all the specifics of RSpec
as there are much better resources out there to learn about the syntax. The scope of this article is to explain how to get RSpec working with Rails 6.0
and Devise
. The code above is simply the most basic example for our purposes.
Now let’s run our test!
Terminal
rspec
But oh no, the test fails! That’s because RSpec
is looking for a HTTP status code of 200
, but got a 302
(redirect). This is because RSpec
isn’t logged in to Devise
yet, so Devise
is redirecting it to the login page, returning 302
.
If we edit our above code a little to:
spec/controllers/articles_controller_spec.rb
# ...
# expect(response).to be_successful # be_successful expects a HTTP Status code of 200
expect(response).to have_http_status(302) # Expects a HTTP Status code of 302
# ...
And re-run the test, it’ll pass! But not being able to access any part of the site behind Devise
isn’t great, so let’s set it up so we can login for tests.
FactoryBot is a great tool for creating test objects in RSpec
. In this case, creating a User for each test.
In your Gemfile
add the following and bundle install
:
Gemfile
group :test do
# Might be other lines here, so simply add after them
gem 'factory_bot_rails'
end
Now let’s configure FactoryBot
. Under /spec
, create a folder called /factories
. In /spec/factories
, create a new file and name it devise.rb
. Populate it with the following:
/spec/factories/devise.rb
FactoryBot.define do
factory :user do
id {1}
email {"test@user.com"}
password {"qwerty"}
# Add additional fields as required via your User model
end
# Not used in this tutorial, but left to show an example of different user types
# factory :admin do
# id {2}
# email {"test@admin.com"}
# password {"qwerty"}
# admin {true}
# end
end
This is the file that will be used to create our User object. Feel free to edit it as necessary.
Next we’ll create the controller macros (the code used to dictate when the User object should be created).
Create a new file called controller_macros.rb
under /spec/support
. Create /support
if it doesn’t exist. Then populate the file with:
/spec/support/controller_macros.rb
module ControllerMacros
def login_user
# Before each test, create and login the user
before(:each) do
@request.env["devise.mapping"] = Devise.mappings[:user]
user = FactoryBot.create(:user)
# user.confirm! # Or set a confirmed_at inside the factory. Only necessary if you are using the "confirmable" module
sign_in user
end
end
# Not used in this tutorial, but left to show an example of different user types
# def login_admin
# before(:each) do
# @request.env["devise.mapping"] = Devise.mappings[:admin]
# sign_in FactoryBot.create(:admin) # Using factory bot as an example
# end
# end
end
Now let’s include all these files into our rails_helper.rb
file. I’ve removed code for the example below, but you shouldn’t remove any code from yours.
/spec/rails_helper.rb
# ...
require 'rspec/rails'
# Add these after require 'rspec/rails'
require 'devise'
require_relative 'support/controller_macros'
# ...
RSpec.configure do |config|
# ...
# Add these
config.include Devise::Test::ControllerHelpers, :type => :controller
config.include FactoryBot::Syntax::Methods
config.extend ControllerMacros, :type => :controller
end
And finally, update articles_controller_spec.rb
file to use our new login_user
(via FactoryBot
) function. Just add the login_user
function at the top of the ArticleController
block. Don’t forget to change the test condition as well towards the bottom.
spec/controllers/articles_controller_spec.rb
require 'rails_helper'
RSpec.describe ArticlesController, type: :controller do
# Add this
login_user
let(:valid_attributes) {
{ :title => "Test title!", :description => "This is a test description", :status => "draft" }
}
let(:valid_session) { {} }
describe "GET #index" do
it "returns a success response" do
Article.create! valid_attributes
get :index, params: {}, session: valid_session
# Make sure to swap this as well
expect(response).to be_successful # be_successful expects a HTTP Status code of 200
# expect(response).to have_http_status(302) # Expects a HTTP Status code of 302
end
end
end
Now simply re-run your test via rspec
and it’ll pass! You can now test your app with Devise
without any issues.
Happy testing! 🎉
DEPRECATION WARNING
If you keep getting:
Terminal
DEPRECATION WARNING: Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper.
Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.
Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload ActionText::ContentHelper, for example,
the expected changes won't be reflected in that stale Module object.
These autoloaded constants have been unloaded.
Please, check the "Autoloading and Reloading Constants" guide for solutions.
Every time you run rspec
and you wanna fix it (as I did), simply do the following:
Open config/enviorments/test.rb
and find the following line:
config/enviorments/test.rb
config.active_support.deprecation = :stderr # Printing to terminal
And change to:
config/enviorments/test.rb
config.active_support.deprecation = :log # Print to log
It won’t fix the issue, but will simply print to a log file so you’re not bothered every time you run your tests.