September 12th, 2019

Setup and test Rails 6.0 with RSpec, FactoryBot, and Devise


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.

Prerequisite

This project assumes you've already setup and configured your Rails 6.0 project. It also assumes you've setup Devise as well.

Update: September 23rd, 2020

Special thanks to Dave for fixing a depreciation warning in the Gemfile file.

Update: July 27th, 2020

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!

Covered in this guide:

  • Install and setup RSpec
  • Install and setup FactoryBot
  • Configure RSpec to work with FactoryBot and Devise
  • Fix the DEPRECATION WARNING: Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper warning

What we’ll be using:

  • Rails 6.0
    • Other versions should work as well with minor tweaks.
  • Ruby 2.5.3
    • Newer versions should work fine.
  • RSpec 4.0 (dev)
    • Most recent RSpec version as of this article.
  • FactoryBot 5.0.2
    • Newer versions should work fine.
  • Devise 4.7.1
    • Newer versions should work fine.

Let’s begin.


Install and configure RSpec

1. Install RSpec

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

Generating Tests

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.

2. Create controller test

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

Devise

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.

Learning RSpec

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.

3. Configure Devise with FactoryBot for RSpec

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! 🎉


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