I have the following test repeated once for each HTTP method/controller action combination within a controller spec:
it "requires authentication" do
get :show, id: project.id
# Unauthenticated users should be redirected to the login page
expect(response).to redirect_to new_user_session_path
end
I've found the three following ways to refactor it and eliminate repetition. Which one is the most appropriate?
It seems to me that shared examples are the most appropriate solution. However, having to use a block in order to pass the params
to the shared example feels a bit awkward.
shared_examples "requires authentication" do |http_method, action|
it "requires authentication" do
process(action, http_method.to_s, params)
expect(response).to redirect_to new_user_session_path
end
end
RSpec.describe ProjectsController, type: :controller do
describe "GET show", :focus do
let(:project) { Project.create(name: "Project Rigpa") }
include_examples "requires authentication", :GET, :show do
let(:params) { {id: project.id} }
end
end
end
This has the advantage of not requiring a block to pass project.id
to the helper method.
RSpec.describe ProjectsController, type: :controller do
def require_authentication(http_method, action, params)
process(action, http_method.to_s, params)
expect(response).to redirect_to new_user_session_path
end
describe "GET show", :focus do
let(:project) { Project.create(name: "Project Rigpa") }
it "requires authentication" do
require_authentication(:GET, :show, id: project.id )
end
end
end
It would be nice to have a single-line test.
RSpec::Matchers.define :require_authentication do |http_method, action, params|
match do
process(action, http_method.to_s, params)
expect(response).to redirect_to Rails.application.routes.url_helpers.new_user_session_path
end
end
RSpec.describe ProjectsController, type: :controller do
describe "GET show", :focus do
let(:project) { Project.create(name: "Project Rigpa") }
it { is_expected.to require_authentication(:GET, :show, {id: project.id}) }
end
end
Thanks in advance.
A suggestion provided by didroe in this Reddit post got me thinking that placing the method/action call (process
) within shared code is not a good idea as it increases complexity (reduces readability) and does not actually reduce code duplication.
After searching some more, I have found what I believe to be best option in the Everyday Rails Testing with RSpec by Aaron Sumner book (p. 102).
Create the following custom matcher:
# spec/support/matchers/require_login.rb
RSpec::Matchers.define :require_login do |expected|
match do |actual|
expect(actual).to redirect_to \
Rails.application.routes.url_helpers.new_user_session_path
end
failure_message do |actual|
"expected to require login to access the method"
end
failure_message_when_negated do |actual|
"expected not to require login to access the method"
end
description do
"redirect to the login form"
end
end
And use a test like the following for each action of each controller:
it "requires authentication" do
get :show, id: project.id
expect(response).to require_login
end
Compared to repeating expect(response).to redirect_to new_user_session_path
in all tests, this approach has the following advantages:
What do you think?
In the case you describe, I would go for RSpec Custom Matchers. They keep your specs easier to read and closer to the domain of you application. https://relishapp.com/rspec/rspec-expectations/v/2-4/docs/custom-matchers/define-matcher
I would use shared_examples to specify more complex scenarios and call it_behaves_like to check it all at once in different contexts.
You should try to avoid helper methods if possible and only use them in a single file if it helps keep your specs clean.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With