Back to home

Devise Part 7: Testing With RSpec And Factory Bot main image

Devise Part 7: Testing With RSpec And Factory Bot

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

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.