Testing RESTful actions of multiple Rails controllers with RSpec can generate a lot of code repetition. The following code is my first attempt at using shared examples to DRY things up.
Here is what I don't like about the code, could not find a better way and would like your help to improve:
- The shared examples require that specific variables are set within
letblocks within the controller spec (high coupling). - The tests that assert redirection use
evalto call Rails' route/path helpers.
Also, please let me know if you spot any additional issues.
The controller spec:
# spec/controllers/quotes_controller_spec.rb
require "rails_helper"
RSpec.describe QuotesController, :focus, :type => :controller do
login_admin
let(:model) { Quote }
let(:record) { FactoryGirl.create(:quote) }
let(:records) { FactoryGirl.create_pair(:quote) }
let(:valid_attributes) do
# The Quote model validates the presence of associated records.
# attributes_for does no create associated records by default.
# For the following code to work, the factory must use "strategy: :create".
FactoryGirl.build(:quote, quote: "New quote").attributes.symbolize_keys.
except(:id, :publish_status, :slug, :user_id, :created_at, :updated_at).
merge({ theme_ids: [FactoryGirl.create(:theme).id] })
end
let(:invalid_attributes) { valid_attributes.update(quote: nil) }
include_examples "GET #index"
include_examples "GET #show"
include_examples "GET #new"
include_examples "GET #edit"
include_examples "POST #create", "quote_path(assigns(:quote))"
include_examples "PATCH #update", "quote_url"
include_examples "DELETE #destroy", "quotes_url"
end
The shared examples:
# spec/support/shared_examples/controller_restful_actions.rb
def ivar_name(model, plural: false)
if plural
model.name.pluralize.underscore.to_sym
else
model.name.underscore.to_sym
end
end
def record_name(model)
model.name.underscore.to_sym
end
RSpec.shared_examples "GET #index" do
describe "GET #index" do
it "requires login" do
sign_out current_user
get :index
expect(response).to require_login
end
it "enforces authorization" do
get :index
expect(controller).to enforce_authorization
end
it "populates instance variable with an array of records" do
get :index
expect(assigns(ivar_name(model, plural: true))).to match_array(records)
end
end
end
RSpec.shared_examples "GET #show" do
describe "GET #show" do
it "requires login" do
sign_out current_user
get :show, id: record
expect(response).to require_login
end
it "enforces authorization" do
get :show, id: record
expect(controller).to enforce_authorization
end
it "assigns the requested record to an instance variable" do
get :show, id: record
expect(assigns(ivar_name(model))).to eq(record)
end
end
end
RSpec.shared_examples "GET #new" do
describe "GET #new" do
it "requires login" do
sign_out current_user
get :new
expect(response).to require_login
end
it "enforces authorization" do
get :new
expect(controller).to enforce_authorization
end
it "assigns a new record to an instance variable" do
get :new
expect(assigns(ivar_name(model))).to be_a_new(model)
end
end
end
RSpec.shared_examples "GET #edit" do
describe "GET #edit" do
let(:record) { FactoryGirl.create(factory_name(model)) }
it "requires login" do
sign_out current_user
get :edit, id: record
expect(response).to require_login
end
it "enforces authorization" do
get :edit, id: record
expect(controller).to enforce_authorization
end
it "assigns the requested record to an instance variable" do
get :edit, id: record
expect(assigns(ivar_name(model))).to eq(record)
end
end
end
RSpec.shared_examples "POST #create" do |redirect_path_helper|
describe "POST #create" do
it "requires login" do
sign_out current_user
post :create, { record_name(model) => valid_attributes }
expect(response).to require_login
end
it "enforces authorization" do
post :create, { record_name(model) => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "saves the new record in the database" do
expect{
post :create, { record_name(model) => valid_attributes }
}.to change(model, :count).by(1)
end
it "assigns a newly created but unsaved record to an instance variable" do
post :create, { record_name(model) => valid_attributes }
expect(assigns(ivar_name(model))).to be_a(model)
expect(assigns(ivar_name(model))).to be_persisted
end
it "redirects to #{redirect_path_helper}" do
post :create, { record_name(model) => valid_attributes }
expect(response).to redirect_to(eval(redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not save the new record in the database" do
expect{
post :create, { record_name(model) => invalid_attributes }
}.not_to change(model, :count)
end
it "assigns a newly created but unsaved record an instance variable" do
post :create, { record_name(model) => invalid_attributes }
expect(assigns(ivar_name(model))).to be_a_new(model)
end
it "re-renders the :new template" do
post :create, { record_name(model) => invalid_attributes }
expect(response).to render_template(:new)
end
end
end
end
RSpec.shared_examples "PATCH #update" do |redirect_path_helper|
describe "PATCH #update" do
let(:record) { FactoryGirl.create(factory_name(model)) }
it "requires login" do
sign_out current_user
patch :update, { :id => record, record_name(model) => valid_attributes }
expect(response).to require_login
end
it "enforces authorization" do
patch :update, { :id => record, record_name(model) => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "updates the requested record" do
patch :update, { :id => record, record_name(model) => valid_attributes }
record.reload
expect(record).to have_attributes(valid_attributes)
end
it "assigns the requested record to an instance variable" do
put :update, { :id => record, record_name(model) => valid_attributes }
expect(assigns(ivar_name(model))).to eq(record)
end
it "redirects to #{redirect_path_helper}" do
patch :update, { :id => record, record_name(model) => valid_attributes }
expect(response).to redirect_to(eval(redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not update the requested record" do
expect {
patch :update, { :id => record, record_name(model) => invalid_attributes }
}.not_to change { record.reload.attributes }
end
it "assigns the record to an instance variable" do
patch :update, { :id => record, record_name(model) => invalid_attributes }
expect(assigns(ivar_name(model))).to eq(record)
end
it "re-renders the :edit template" do
patch :update, { :id => record, record_name(model) => invalid_attributes }
expect(response).to render_template(:edit)
end
end
end
end
RSpec.shared_examples "DELETE #destroy" do |redirect_path_helper|
describe "DELETE #destroy" do
it "requires login" do
sign_out current_user
delete :destroy, id: record
expect(response).to require_login
end
it "enforces authorization" do
delete :destroy, id: record
expect(controller).to enforce_authorization
end
it "deletes the record" do
# Records are lazily created. Here we must force its creation.
record
expect{
delete :destroy, id: record
}.to change(model, :count).by(-1)
end
it "redirects to #{redirect_path_helper}" do
delete :destroy, id: record
expect(response).to redirect_to(eval(redirect_path_helper))
end
end
end
Aucun commentaire:
Enregistrer un commentaire