Home Rails & RSpec: Testing CRUD actions with shared examples
Reply: 0

Rails & RSpec: Testing CRUD actions with shared examples

user13293
1#
user13293 Published in September 20, 2018, 1:18 pm

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 let blocks within the controller spec (high coupling). I have tried to use the model name to infer the factory name and create the test data within the share examples. It works well to create the record and records variables. However, some models require the presence of associations and FactoryGirl.attributes_for does not create associated records, so validation fails. So, valid_attributes are created differently for different models. The only (likely bad) way I could think of creating valid_attributes within shared examples is to pass a string containing the code used to create the attributes and evaluate it (eval) within the shared examples
  • The tests that assert redirection use eval to call Rails' route/path helpers. Different controllers in this app have different redirect behaviors. After creating or updating a record, some controllers redirect to the #show action, others to #index. The problem is that when expecting a redirect to #show, AFAIK, we have to know the record ID in order to build the expected URL. And we don't know the record ID within the controller spec. We only know it within the shared examples. So how can we pass an expected redirect URL from the controller spec to the shared example if we do not yet know what that URL is (because we don't know the record ID)?

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) { FactoryGirl.attributes_for(:quote, quote: "New quote") }
  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
share|improve this question

3 Answers 3

active oldest votes
up vote 2 down vote accepted
+50
You need to login account before you can post.

About| Privacy statement| Terms of Service| Advertising| Contact us| Help| Sitemap|
Processed in 0.543652 second(s) , Gzip On .

© 2016 Powered by mzan.com design MATCHINFO