ruby-on-rails - rails 和 RSpec : Testing CRUD actions with shared examples

标签 ruby-on-rails ruby testing rspec

使用 RSpec 测试多个 Rails Controller 的 RESTful 操作会产生大量代码重复。以下代码是我第一次尝试使用共享示例来 DRY 事情。

以下是我不喜欢的代码,找不到更好的方法并希望您帮助改进:

  • 共享示例要求在 Controller 规范(高耦合)的 let block 中设置特定变量。我尝试使用模型名称来推断工厂名称并在共享示例中创建测试数据。它可以很好地创建记录和记录变量。但是,某些模型需要存在关联,而 FactoryGirl.attributes_for 不会创建关联记录,因此验证失败。因此,为不同的模型创建不同的 valid_attributes。我能想到的在共享示例中创建 valid_attributes 的唯一(可能不好)方法是传递一个包含用于创建属性的代码的字符串,并在共享示例中对其进行评估 (eval)
  • 断言重定向的测试使用 eval 来调用 Rails 的路由/路径助手。此应用中的不同 Controller 具有不同的重定向行为。创建或更新记录后,一些 Controller 重定向到#show 操作,其他 Controller 重定向到#index。问题是,当期望重定向到 #show,AFAIK 时,我们必须知道记录 ID 才能构建预期的 URL。而且我们不知道 Controller 规范中的记录 ID。我们只在共享示例中知道它。那么,如果我们还不知道该 URL 是什么(因为我们不知道记录 ID),我们如何将 Controller 规范中的预期重定向 URL 传递到共享示例?

此外,如果您发现任何其他问题,请告诉我。

Controller 规范:

# 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

共享示例:

# 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

最佳答案

可能不是答案,但评论时间太长:

首先,您可以将所有这些包装在 shared_examples_for block 中,例如

shared_examples_for 'a CRUD Controller' do 
  context "GET #index" do
    it "requires login" do
      sign_out current_user
      get :index
      expect(response).to require_login
    end
   ####
  end
  context "GET #show" do
    it "requires login" do
      sign_out current_user
      get :show, id: record
      expect(response).to require_login
    end
   ####
  end
end

其次You can have shared examples inside shared examples to the above can be

shared_examples_for 'a CRUD Controller' do 
  shared_examples_for 'authenticatable' do |view:,params:{}|
    it "requires login" do
      sign_out current_user
      get view, **params
      expect(response).to require_login
    end
  end

  context "GET #index" do
   it_behaves_like 'authenticatable', view: :index 
   ####
  end
  context "GET #show" do
   it_behaves_like 'authenticatable', view: :show, id: record
   ####
  end
end

第三,您可以在 it_behaves_like block 内分配变量,例如。

RSpec.describe QuotesController, :focus, :type => :controller do
  login_admin
  it_behaves_like 'a CRUD Controller' do  
    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) }
  end
end

第四这个也可以简化

shared_examples_for 'a CRUD Controller' do |model:| 
  singular,plural = 2.times.map { |n| model.name.pluralize(n).underscore.to_sym }
  let(:record) { FactoryGirl.create(singular)
  let(:records) {FactoryGirl.create_pair(singular) }
  let(:valid_attributes) do 
    # build should create the nested associations correctly as long 
    # as your factories are right
    FactoryGirl.build(singular).attributes.delete_if do |k,_| 
      # this is because ActiveRecord#attributes contains columns 
      # you don't want to be considered updateable
      ["id","created_at","updated_at"].include?(k)
    end 
  end 
  let(:invalid_attributes) do 
    # create an :invalid trait in your factory so that 
    # you don't have to worry about the model
    FactoryGirl.build(singular, :invalid).attributes.delete_if do |k,_| 
      ["id","created_at","updated_at"].include?(k)
    end 
  end 
  ####
end

RSpec.describe QuotesController, :focus, :type => :controller do
  login_admin
  it_behaves_like 'a CRUD Controller', model: Quote
end

最后,您会发现使用备忘的 let! 将大有帮助,因为您正在这些测试中创建大量的记录,就目前而言。这将大大降低性能,如果您使用的模型具有某些全局唯一属性,您的测试将在任何地方都失败。

希望这有助于开始为您指明正确的方向

更新以控制测试操作

shared_examples_for 'a CRUD Controller' do |model:|
  accessible_method = ->(meth) { public_methods.include?(meth) }

  context "GET #index", if: controller.method_defined?(:index) do
    it_behaves_like 'authenticatable', view: :index 
    ####
  end
  context "GET #show", if: controller.method_defined?(:show) do
    it_behaves_like 'authenticatable', view: :show, id: record
    ####
  end 
end

关于ruby-on-rails - rails 和 RSpec : Testing CRUD actions with shared examples,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44957628/

相关文章:

ruby-on-rails - Faker 在factory_girl 中使用时会产生重复数据

ruby - 尽管被列入白名单,但 ActiveAdmin 未经允许的参数

ruby-on-rails - Rails 测试 has_many 关联失败

ruby-on-rails - 无法读取未定义的属性 'asSorting' - DataTables

ruby-on-rails - Redis::TimeoutError: 连接超时错误-Rails 缓存

ruby-on-rails - 使用 RubyInstaller 1.9.1 RC2 后在 Windows 上启动 mongrel 时出错

ruby-on-rails - 在 Rails 中,我可以检查上传的 zip 文件是否损坏或无效吗?

ruby-on-rails - 在 activerecord 中自动重命名外键

ruby-on-rails - ActiveRecord::Fixture::FixtureError:表 "users"没有名为 "status"的列

javascript - Ajax 驱动的 JavaScript 运行时断言框架