How to test User login using Devise and Rails 7 test controllers
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
fixturestest_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_PASSWORD
on 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.