Devise Part 7: Testing With RSpec And Factory Bot

For this part, we look at setting up some helpers for us to log users in when writing controller tests.

On this page

Part seven will demonstrate how to use FactoryBot to create test data and how to set up RSpec and Devise.

We will be adding in a new Document model to our application that we will write a simple test for to demonstrate this.

Source code can be found here.

Prerequisites

Getting started

We will be working from the source code found here.

# Clone and change into the project
$ git clone https://github.com/okeeffed/demo-rails-7-with-devise-series
$ cd demo-rails-7-with-devise-series

# Continue from part 6 - warning that Redis required
$ git checkout 6-recaptcha

# We will use rspec-rails to test our controllers and auth
$ bundler add rspec-rails --group="development,test"
# We'll use FactoryBot for testing
$ bundler add factory_bot_rails --group "development,test"

# Setup RSpec
$ bin/rails g rspec:install

# Create a document controller without a view
$ bin/rails g controller documents --skip-template-engine
# Generate our Document model
$ bin/rails g model Document body:string

# Create spec for testing the document
$ mkdir -p spec/controllers
# Create a test file for our spec
$ touch spec/controllers/documents_controller_spec.rb

# Make User factory
$ mkdir -p spec/factories
$ mkdir -p spec/support
$ touch spec/support/factory_bot.rb
# Create the user factory and document factory
$ touch spec/factories/user_factory.rb spec/factories/document_factory.rb

# Configuring Devise for RSpec
$ touch spec/support/controller_macros.rb

In the above setup, we installed the required gems and the scaffold out a lot of the files and folders that we will need for setting up our tests.

We also add a Document model with a simple body type.

Setting up our many-to-many relationship

We want to actually make the document and user models a many-to-many relationship, so let's tackle that next:

# Create join table
$ bin/rails g migration CreateJoinTableUsersDocuments users documents

We need to update both the Document and User models.

For Documents, it should now look like this:

class Document < ApplicationRecord
  has_and_belongs_to_many :users
end

For Users:

class User < ApplicationRecord
  has_and_belongs_to_many :documents

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: %i[github]

  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20]
      # user.name = auth.info.name   # assuming the user model has a name
      # user.image = auth.info.image # assuming the user model has an image
      # If you are using confirmable and the provider(s) you use validate emails,
      # uncomment the line below to skip the confirmation emails.
      # user.skip_confirmation!
    end
  end
end

Configure the documents controller routes

Next, we need to update the config/routes.rb file for our new document endpoints:

Rails.application.routes.draw do
  devise_for :users,
             controllers: { omniauth_callbacks: 'users/omniauth_callbacks', sessions: 'users/sessions',
                            registrations: 'users/registrations' }
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  resources :users
  resources :home, only: %i[index create]
  resources :session, only: [:index]
  # ADD HERE
  resources :documents, only: %i[index create update destroy]
  root 'home#index'
end

Configuring the Rails helper

In the spec/rails_helper.rb file, uncomment the line Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } as well as include our required helpers.

It should look like the following:

# This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
# Prevent database truncation if the environment is production
abort('The Rails environment is running in production mode!') if Rails.env.production?
require 'rspec/rails'
# ... omitted ...
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

# ... omitted ...
RSpec.configure do |config|
  # ... omitted ...

  config.include Devise::Test::ControllerHelpers, type: :controller
  config.extend ControllerMacros, type: :controller
end

Note: I omitted the unchanged code above.

Setting up our factories

We will setup two factories:

  1. A User factory.
  2. A Document factory.

For spec/factories/document_factory.rb:

FactoryBot.define do
  factory :document do
    body { 'Hello, world' }
  end
end

For spec/factories/user_factory.rb:

FactoryBot.define do
  factory :user do
    id { 2 }
    email { 'hello@example.com' }
    password { 'password123' }
  end
end

Next is setting up our helpers for logging users in for devise.

Configuring the spec support files

The spec/support/controller_macros.rb will have a helper to login in a user login_user and looks like this:

module ControllerMacros
  def login_user
    # Before each test, create and login the user
    before(:each) do
      @request.env['devise.mapping'] = Devise.mappings[:user]
      sign_in FactoryBot.create(:user)
    end
  end
end

Finally, we can add a support file for FactoryBot in the spec/support/factory_bot.rb file:

RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

This could be added to the rails_helper.rb config file instead too if you wanted.

Filling in the DocumentController code

Inside of the app/controllers/documents_controller.rb file, fill in the following:

class DocumentsController < ApplicationController
  def create
    @doc = Document.new(body: params[:body])
    @doc.save!

    render json: @doc, status: :created
  end

  def index
    @docs = Document.all
    render json: @docs
  end

  def update
    @doc = Document.find(params[:id])
    @doc.update!(document_params)
    render json: @doc
  end

  def destroy
    @doc = Document.find(params[:id])
    @doc.destroy
    render status: :no_content
  end
end

Writing our test

Finally, we can write a test to check that user is logged in before they can access the document.

In spec/controllers/documents_controller_spec.rb:

require 'rails_helper'

RSpec.describe DocumentsController, type: :controller do
  describe 'GET #index' do
    let(:subject) { create(:document) }

    context 'successful responses' do
      login_user
      it 'returns all posts when user is authorized' do
        get :index, params: { body: subject.body }

        expect(response.status).to eq(200)
        expect(response.parsed_body).to eq([subject.as_json])
      end
    end

    context 'unsuccessful responses' do
      it 'redirects user when they are not logged in' do
        get :create, params: { body: subject.body }

        expect(response.status).to eq(302)
      end
    end
  end
end

Our first test expects us to return an array of all documents (which we create one using factory bot).

The second test will check that we are redirected to the sign-in page (as that is our current behavior in the app).

We can now run our tests to see if they run as expected:

# Run RSpec
$ bundler exec rspec
..

Finished in 0.15058 seconds (files took 2.37 seconds to load)
2 examples, 0 failures

Success!

Summary

In this part of the series, we demonstrated how we can begin writing tests that requirements around a user login when interacting with our document controller.

The next part in the series will move on from authentication to authorization and how we can use the Pundit gem to do just that.

Resources and further reading

Photo credit: lightcircle