How to test User login using Devise and Rails 7 test controllers

Jason Bornhoft
4 min readJan 11, 2023

Being able to test User login using Devise is an important part of any Rails 7 test suite, especially because we specifically want to ensure that certain resources are only accessible to logged in users and non-logged in users are redirected. This blog post walks through how to write your code, tests and fixtures to ensure that your tests work as desired.

Here are the versions being used:

  • devise (4.8.1)
  • minitest (5.16.3)
  • rails (7.0.1)

Let’s consider a situation where we are writing code for an institution that has courses and we want to ensure that the course resources are only available to logged in users. We’ll need the following resources:

  • models/user.rb
  • courses_controller.rb
  • courses & users fixtures
  • test_helper.rb
  • courses_controller_test.rb

User model

The devise portion of the user model looks like this:

class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :trackable, :confirmable

The above are the devise modules that are currently implemented in this example. The most significant for the purposes of this blog is :confirmable which we will come back to later on. This module requires that a user confirm their email address in order to be able to properly access the application.

Courses controller

Here’s an partial example course_controller.rb that works for us in this scenario. For the sake of brevity only the index method is included:

class CoursesController < ApplicationController
before_action :authenticate_user!

def index
@courses = Course.all
end

The above block ensures that no one is able to view the index template without authenticating the user.

Courses fixture

one:
name: 'Course One'
description: 'MyText'

This example will be calling the index method so we’re adding a single course to the courses.yml fixtures file.

Users fixture

admin:
email: 'admin@example.com'
encrypted_password: <%= Devise::Encryptor.digest(User, ENV['TEST_PASSWORD']) %>
confirmed_at: <%= Time.zone.now - 1.hour %>
confirmation_sent_at: <%= Time.zone.now - 2.hours %>
admin: true

As mentioned earlier, this example uses the :confirmable module therefore requiring the two related lines confirmed_at & confirmation_sent_at. This tells devise that the email address is already confirmed and to allow the user to access restricted resources.

Note: In the above use case the password is saved as a local environment variable called TEST_PASSWORDon the local workstation. This is to discourage the inclusion of plain text passwords in code repositories.

Test helper

We need to make a single line change to the test_helper.rb which should now look like this:

ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'

module ActiveSupport
class TestCase
# Run tests in parallel with specified workers
parallelize(workers: :number_of_processors)

# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all

include Devise::Test::IntegrationHelpers
end
end

The change we made was to add the include line which contains the sign_in which we’re going to use in the courses_controller_test.rb file.

Course controller (test)

There are three relevant sections to this portion of the courses_controller_test.rb file. The first thing we do is use a setup block which signs in the admin from the users.yml fixtures file. The next two sections are the tests themselves. The first test ensures that a logged in user, admin in this case, able to the access the courses_url which requires authentication. The second test logs out the user and ensures that the user gets redirected to the login page.

class CoursesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:admin)
@course = courses(:one)
end

test 'should get index if logged in' do
get courses_url
assert_response :success
end

test 'should get redirected if not logged in' do
sign_out :user
get courses_url
assert_response :redirect
end

Results

If the admin user is not signed in then the following results will be received:

$ rails test test/controllers/courses_controller_test.rb 
Running 2 tests in a single process (parallelization threshold is 50)
Run options: --seed 36126

# Running:

.F

Failure:
CoursesControllerTest#test_should_get_index_if_logged_in [/home/jason/coding/project/test/controllers/courses_controller_test.rb:14]:
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/users/sign_in>
Response body: <html><body>You are being <a href="http://www.example.com/users/sign_in">redirected</a>.</body></html>


rails test test/controllers/courses_controller_test.rb:12



Finished in 0.142919s, 13.9939 runs/s, 13.9939 assertions/s.
2 runs, 2 assertions, 1 failures, 0 errors, 0 skips

You can see that there is no logged in user and the test is getting a 302 redirect instead of a 200 success. Once all of the above code is in place then the output is as follows:

$ rails test test/controllers/courses_controller_test.rb 
Running 2 tests in a single process (parallelization threshold is 50)
Run options: --seed 35488

# Running:

..

Finished in 0.166630s, 12.0026 runs/s, 12.0026 assertions/s.
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

This shows that both tests were successful thanks to the ability to log in a user and test restricted access and redirects where access should not be granted.

Conclusion

The importance of proper testing and appropriate coverage cannot be overstated and the above code snippets demonstrate that it is quite straightforward to test different scenarios where it is required to be logged in to access particular resources.

--

--

Jason Bornhoft

Over 10 years in DevOps and 5+ in the Security space. A big fan of concise, clean code with sane defaults.