Controller Tests with RSpec and Rails 8 Authentication
Published on 2025-07-02
We're all writing tests for our side projects right? We're ensuring that code is reliable for
for all those users we have! I recently decided that my project, all it's users (hint: that's only me)
would benefit from some controller tests and I realized that I didn't have anything set up in the
tests to authenticate a User
. That poses a bit of a problem in writing any useful tests, however
since all the authentication code already exists in our repository it is only matter of adding a
helper method to our test environment. Thankfully the Rails community was two steps ahead of me
and had already delivered on a solution. Let's do a quick walkthrough of what has changed in the
Rails 8 authentication generator, how we can apply those changes to RSpec, and wrap up with a passing
controller test.
Test Helper - Rails 8 Authentication Generator#
Having been a long time devise user, I'm accustomed to throwing
a login_user
call at the top of a controller test and being on my way. However, we're not using devise
anymore
and I wondered if anyone had implemented a similar solution for the Rails 8 authentication generator? I
googled (in this day and age?!) rails 8 authentication tests and the top result was a GitHub issue on the
Rails repository, Controller testing new authentication generator.
This seemed like a good place to start so I read through all the comments and there are a ton of helpful insights.
At the end of the discussion, a Pull Request was opened
Add login_as(user) testing helper when generating authentication.
This is pretty exciting and it means that the authentication generator wasn't a one and done piece of work.
The Rails team and community see it as a core piece of the framework and something that can and will evolve
over time. The Pull Request is a small 44 lines of change so take a quick look at it but the actual helper
function can be seen below.
module SessionTestHelper
def sign_in_as(user)
Current.session = user.sessions.create!
ActionDispatch::TestRequest.create.cookie_jar.tap do |cookie_jar|
cookie_jar.signed[:session_id] = Current.session.id
cookies[:session_id] = cookie_jar[:session_id]
end
end
def sign_out
Current.session&.destroy!
cookies.delete(:session_id)
end
end
module SessionTestHelper
def sign_in_as(user)
Current.session = user.sessions.create!
ActionDispatch::TestRequest.create.cookie_jar.tap do |cookie_jar|
cookie_jar.signed[:session_id] = Current.session.id
cookies[:session_id] = cookie_jar[:session_id]
end
end
def sign_out
Current.session&.destroy!
cookies.delete(:session_id)
end
end
Adding the Test Helper to RSpec#
The generator code in the Pull Request makes assumptions that we're using Minitest,
which is the default testing framework so that's expected. However, I've been a long time RSpec user so I wanted to adapt
the helper function to my RSpec setup. Here's the file I created.
spec/support/authentication_helpers.rb
# Lifted from https://github.com/rails/rails/pull/53708 - Add login_as(user) testing helper when generating authentication
RSpec.shared_context "authentication helpers" do
def sign_in_as(user)
Current.session = user.sessions.create!
ActionDispatch::TestRequest.create.cookie_jar.tap do |cookie_jar|
cookie_jar.signed[:session_id] = Current.session.id
cookies[:session_id] = cookie_jar[:session_id]
end
end
def sign_out
Current.session&.destroy!
cookies.delete(:session_id)
end
end
RSpec.configure { |config| config.include_context "authentication helpers" }
# Lifted from https://github.com/rails/rails/pull/53708 - Add login_as(user) testing helper when generating authentication
RSpec.shared_context "authentication helpers" do
def sign_in_as(user)
Current.session = user.sessions.create!
ActionDispatch::TestRequest.create.cookie_jar.tap do |cookie_jar|
cookie_jar.signed[:session_id] = Current.session.id
cookies[:session_id] = cookie_jar[:session_id]
end
end
def sign_out
Current.session&.destroy!
cookies.delete(:session_id)
end
end
RSpec.configure { |config| config.include_context "authentication helpers" }
Note that I didn't change any of the code for sign_in_as
or sign_out
. The main difference is removing
module SessionTestHelper
and replacing with RSpec.shared_context "authentication helpers" do
along with the
final line RSpec.configure { |config| config.include_context "authentication helpers" }
. There is one
more piece that is needed, which is to include this context in all our tests.
spec/rails_helper.rb
Dir[Rails.root.join("spec", "support", "**", "*.rb")].each { |f| require f }
Dir[Rails.root.join("spec", "support", "**", "*.rb")].each { |f| require f }
Now we're able to use this authentication helper! Let's write a quick test to ensure everything is working
as expected.
Example Test#
In one of my previous posts about using the authentication generator,
Adding Email Address Verification in Rails 8, we set up
a UsersController
to create new users and an EmailAddressVerificationsController
to have the user
confirm the email address they used. As I alluded to in the beginning of this post, I hadn't written any
tests for my project using that code so let's go ahead and write some tests for the
EmailAddressVerificationsController
.
spec/controllers/email_address_verifications_controller_spec.rb
require "rails_helper"
RSpec.describe EmailAddressVerificationsController do
let(:user) { FactoryBot.create(:user) }
let(:other_user) { FactoryBot.create(:user) }
before(:each) do
sign_in_as(user)
end
describe "#show" do
context "when the verification token is valid and user matches" do
it "verifies the user and returns success" do
get :show, params: { token: user.email_address_verification_token }
expect(response).to have_http_status(:ok)
# Add expectation around rendered JSON or redirected path
end
end
context "when the verification token belongs to a different user" do
it "returns an error for user mismatch" do
get :show, params: { token: other_user.email_address_verification_token }
expect(response).to have_http_status(:unprocessable_entity)
# Add expectation around rendered JSON or redirected path
end
end
end
describe "#resend" do
it "resends the verification email" do
expect { post :resend }.to have_enqueued_mail(UserMailer, :verify_email_address)
expect(response).to have_http_status(:ok)
# Add expectation around rendered JSON or redirected path
end
end
end
require "rails_helper"
RSpec.describe EmailAddressVerificationsController do
let(:user) { FactoryBot.create(:user) }
let(:other_user) { FactoryBot.create(:user) }
before(:each) do
sign_in_as(user)
end
describe "#show" do
context "when the verification token is valid and user matches" do
it "verifies the user and returns success" do
get :show, params: { token: user.email_address_verification_token }
expect(response).to have_http_status(:ok)
# Add expectation around rendered JSON or redirected path
end
end
context "when the verification token belongs to a different user" do
it "returns an error for user mismatch" do
get :show, params: { token: other_user.email_address_verification_token }
expect(response).to have_http_status(:unprocessable_entity)
# Add expectation around rendered JSON or redirected path
end
end
end
describe "#resend" do
it "resends the verification email" do
expect { post :resend }.to have_enqueued_mail(UserMailer, :verify_email_address)
expect(response).to have_http_status(:ok)
# Add expectation around rendered JSON or redirected path
end
end
end
One difference between this setup and the login_user
method, which takes no arguments, from devise
is that
we have to reference a created User
record and in RSpec that means sign_in_as
has to be called inside
of a scoping block. If we remove the before(:each)
and try to call it, we see the following error.
An error occurred while loading ./spec/controllers/email_address_verifications_controller_spec.rb.
Failure/Error: sign_in_as(user)
user
is not available on an example group (e.g. a describe
or context
block). It is only available from within
individual examples (e.g. it
blocks) or from constructs that run in the scope of an example (e.g. before
, let
, etc).
Let's go ahead and verify that all the tests are passing.
rspec spec/controllers/email_address_verifications_controller_spec.rb
...
3 examples, 0 failures
rspec spec/controllers/email_address_verifications_controller_spec.rb
...
3 examples, 0 failures
Conclusion#
The Rails community is awesome and I was so happy to see this addition to the authentication generator was
community driven. I hope that the RSpec users in the community who haven't started writing any tests yet are
able to benefit from this post. Happy testing!