You are writing a spec with type: :request
, i.e. an integration spec instead
of a controller spec. Integration specs are wrappers around Rails'
ActionDispatch::IntegrationTest
class. I usually write controller tests using
this instead of type: :controller
, mainly because it exercises more of the
request and response handling stack. So instead of writing something like
get :index
to start the request, you would write get books_path
or similar.
One of the issues with using type: :request
is that you lose the ability to
set session variables before issuing a request. Integration tests assume that
you will always start a test from an initial state, and mutate the state by
issuing requests. I think this is fine if your application is self-contained.
Where it falls apart is when your server application has complex request
flows involving redirections to third party services (eg. single sign on). In
this kind of flow, we might want to start the test from a midway state simply
because it is far easier to orchestrate a desired state to test a particular
branch (eg. error states). Sometimes the requirements of these flows involve
the use of sessions so we cannot avoid making it stateless. What to do then?
Unfortunately there is no convenient way to set the session variables in integration tests. At the beginning of the test, the session object is undefined. It only gets defined after a request is made. Even then, the session object is read-only -- modifying it will not carry over the changes to the next request.
A solution or strategy that I settled on:
- Make a route that is only mounted in the test environment.
- The route handler takes parameters from the request and assigns those parameters to the session.
- The integration test sets session variables by issuing the request to our test-only route.
Sample code follows.
# config/routes.rb
Rails.application.routes.draw do
if Rails.env.test?
namespace :test do
resource :session, only: %i[create]
end
end
end
# app/controllers/test/sessions_controller.rb
module Test
class SessionsController < ApplicationController
def create
vars = params.permit(session_vars: {})
vars[:session_vars].each do |var, value|
session[var] = value
end
head :created
end
end
end
require 'rails_helper'
describe 'some controller or function', type: :request do
def set_session(vars = {})
post test_session_path, params: { session_vars: vars }
expect(response).to have_http_status(:created)
vars.each_key do |var|
expect(session[var]).to be_present
end
end
it 'will do something' do
set_session(session_var_1: 'foobar', session_var_2: 'something else')
# the rest of your test as usual.
get some_function_path
expect(response).to be_redirect
end
end
π€ hah!, this is actually great idea.
warning
Reason why RSpec dropped controller specs in favor of request tests is because Rails core dropped them and RSpec-Rails is trying to stay consistent with Rails core
Reason why Rails dropped the controller tests in favor of integration (request tests) => so that developers test the entire flow rather than stub the π© out of their controllers
So the way how Rails team would advise to test session stuff like "user signed in" is to actually call the user sign endpoint before calling the endpoint which we want to testβ οΈ
That being said there are definitely scenarios where this would be more than just inconvenience (e.g. you need to call multiple endpoints that sets some complex session steps beyond simple user login) and this is where this solution comes handy π