# 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.

- Canonical: https://dennisokeeffe.com/blog/2022-03-09-part-7-testing-with-rspec-and-factory-bot
- Published: 2022-03-09
- Updated: 2022-03-09
- Tags: ruby, ruby-on-rails

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](https://github.com/okeeffed/demo-rails-7-with-devise-series).

## Prerequisites

- Catch up with previous parts in this series [Devise with Ruby on Rails 7](https://blog.dennisokeeffe.com/blog/series/devise-with-ruby-on-rails-7).
- Read my ["FactoryBot with Rails"](https://blog.dennisokeeffe.com/blog/2022-02-24-factory-bot-with-rails) blog post.

## Getting started

We will be working from the source code found [here](https://github.com/okeeffed/demo-rails-7-with-devise-series).

```s
# 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:

```s
# 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:

```rb
class Document < ApplicationRecord
  has_and_belongs_to_many :users
end
```

For Users:

```rb
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:

```rb
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:

```rb
# 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`:

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

For `spec/factories/user_factory.rb`:

```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:

```rb
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:

```rb
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:

```rb
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`:

```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:

```s
# 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](https://github.com/varvet/pundit) gem to do just that.

## Resources and further reading

- [Devise with Ruby on Rails 7](https://blog.dennisokeeffe.com/blog/series/devise-with-ruby-on-rails-7)
- [Pundit](https://github.com/varvet/pundit)
- [Source code](https://github.com/okeeffed/demo-rails-7-with-devise-series)

_Photo credit: [`lightcircle`](https://unsplash.com/@lightcircle)_
