使用RSpec测试多个Rails控制器的RESTful操作可以生成大量代码重复。以下代码是我第一次尝试使用共享示例来解决问题。
以下是我对代码不喜欢的内容,无法找到更好的方法并希望您的帮助有所改善:
let
块内设置特定变量。我试图使用模型名称来推断工厂名称并在共享示例中创建测试数据。它可以很好地创建记录和记录变量。但是,某些模型需要存在关联,而FactoryGirl.attributes_for不会创建关联记录,因此验证失败。因此,对于不同的模型,valid_attributes的创建方式不同。我可以想到在共享示例中创建valid_attributes的唯一(可能是坏的)方法是传递一个字符串,其中包含用于创建属性的代码并在共享示例中对其进行评估(eval)eval
来调用Rails'路线/路径助手。此应用程序中的不同控制器具有不同的重定向行为创建或更新记录后,某些控制器会重定向到#show操作,其他控制器会重定向到#index。问题是当期望重定向到#show,AFAIK时,我们必须知道记录ID才能构建预期的URL。而且我们不知道控制器规范中的记录ID。我们只在共享示例中知道它。那么,如果我们还不知道该URL是什么(因为我们不知道记录ID),我们如何将预期的重定向URL从控制器规范传递到共享示例?另外,如果您发现任何其他问题,请告诉我。
控制器规格:
# 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
答案 0 :(得分:2)
可能不是答案,但评论时间太长了:
首先,您可以将所有这些内容包装在shared_examples_for
块中,例如
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
其次你可以在共享示例中共享示例,上面的内容可以是
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
块内分配变量,例如
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
最后,你会发现使用memoized 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
答案 1 :(得分:1)
这是改进的代码(基于engineermnky的建议)。欢迎任何进一步改进的建议。
控制器规格:
# spec/controllers/quotes_controller_spec.rb
require "rails_helper"
RSpec.describe QuotesController, :type => :controller do
it_behaves_like "a CRUD controller",
model: Quote,
create_redirect_path_helper: "quote_path(assigns(:quote))",
update_redirect_path_helper: "quote_url",
delete_redirect_path_helper: "quotes_url"
end
共享示例:
# spec/support/shared_examples/controller_restful_actions.rb
RSpec.shared_examples "a CRUD controller" do |model:,
create_redirect_path_helper:,
update_redirect_path_helper:,
delete_redirect_path_helper:|
def self.controller_has_action?(action)
described_class.action_methods.include?(action.to_s)
end
resource_singular = model.name.underscore.to_sym
resource_plural = model.name.pluralize.underscore.to_sym
before(:each) { login_admin }
let(:record) { FactoryGirl.create(resource_singular) }
let(:records) { FactoryGirl.create_pair(resource_singular) }
# Models that validate the presence of associated records require some
# hacking in the factory to include associations in the attributes_for output.
let(:valid_attributes) { FactoryGirl.attributes_for(resource_singular) }
# All factories must have a trait called :invalid
let(:invalid_attributes) do
FactoryGirl.attributes_for(resource_singular, :invalid)
end
describe "GET #index", if: controller_has_action?(:index) do
it "requires login" do
logout
get :index
expect(response).to require_login_web
end
it "enforces authorization" do
get :index
expect(controller).to enforce_authorization
end
it "populates @#{resource_plural} with an array of #{resource_plural}" do
# Force records to be created before the request.
records
get :index
# Required when testing the User model, or else the user created
# by the Devise login helper skews the result of this test.
expected_records = assigns(resource_plural) - [@current_user]
expect(expected_records).to match_array(records)
end
end
describe "GET #show", if: controller_has_action?(:show) do
it "requires login" do
logout
get :show, id: record
expect(response).to require_login_web
end
it "enforces authorization" do
get :show, id: record
expect(controller).to enforce_authorization
end
it "assigns the requested #{resource_singular} to an instance variable" do
get :show, id: record
expect(assigns(resource_singular)).to eq(record)
end
end
describe "GET #new", if: controller_has_action?(:new) do
it "requires login" do
logout
get :new
expect(response).to require_login_web
end
it "enforces authorization" do
get :new
expect(controller).to enforce_authorization
end
it "assigns a new #{resource_singular} to @#{resource_singular}" do
get :new
expect(assigns(resource_singular)).to be_a_new(model)
end
end
describe "GET #edit", if: controller_has_action?(:edit) do
it "requires login" do
logout
get :edit, id: record
expect(response).to require_login_web
end
it "enforces authorization" do
get :edit, id: record
expect(controller).to enforce_authorization
end
it "assigns #{resource_singular} to @#{resource_singular}" do
get :edit, id: record
expect(assigns(resource_singular)).to eq(record)
end
end
describe "POST #create", if: controller_has_action?(:create) do
it "requires login" do
logout
post :create, { resource_singular => valid_attributes }
expect(response).to require_login_web
end
it "enforces authorization" do
post :create, { resource_singular => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "saves the new #{resource_singular} in the database" do
expect{
post :create, { resource_singular => valid_attributes }
}.to change(model, :count).by(1)
end
it "assigns the saved #{resource_singular} to @#{resource_singular}" do
post :create, { resource_singular => valid_attributes }
expect(assigns(resource_singular)).to be_an_instance_of(model)
expect(assigns(resource_singular)).to be_persisted
end
it "redirects to #{create_redirect_path_helper}" do
post :create, { resource_singular => valid_attributes }
expect(response).to redirect_to(eval(create_redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not save the new #{resource_singular} in the database" do
expect{
post :create, { resource_singular => invalid_attributes }
}.not_to change(model, :count)
end
it "assigns the unsaved #{resource_singular} to @#{resource_singular}" do
post :create, { resource_singular => invalid_attributes }
expect(assigns(resource_singular)).to be_a_new(model)
end
it "re-renders the :new template" do
post :create, { resource_singular => invalid_attributes }
expect(response).to render_template(:new)
end
end
end
describe "PATCH #update", if: controller_has_action?(:update) do
it "requires login" do
logout
patch :update, { :id => record,
resource_singular => valid_attributes }
expect(response).to require_login_web
end
it "enforces authorization" do
patch :update, { :id => record,
resource_singular => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "updates the requested #{resource_singular}" do
patch :update, { :id => record,
resource_singular => valid_attributes }
record.reload
# Required when testing Devise's User model with reconfirmable on
record.try(:confirm)
expect(record).to have_attributes(valid_attributes)
end
it "assigns the #{resource_singular} to @#{resource_singular}" do
put :update, { :id => record,
resource_singular => valid_attributes }
expect(assigns(resource_singular)).to eq(record)
end
it "redirects to #{update_redirect_path_helper}" do
patch :update, { :id => record,
resource_singular => valid_attributes }
expect(response).to redirect_to(eval(update_redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not update the #{resource_singular}" do
# Do not attempt to "refactor" the following to any of the following:
# not_to change { quote }
# not_to change { quote.attributes }
# not_to have_attributes(invalid_attributes)
# None of the above will work. See
# https://github.com/rspec/rspec-expectations/issues/996#issuecomment-310729685
expect {
patch :update, { :id => record,
resource_singular => invalid_attributes }
}.not_to change { record.reload.attributes }
end
it "assigns the #{resource_singular} to @#{resource_singular}" do
patch :update, { :id => record,
resource_singular => invalid_attributes }
expect(assigns(resource_singular)).to eq(record)
end
it "re-renders the :edit template" do
patch :update, { :id => record,
resource_singular => invalid_attributes }
expect(response).to render_template(:edit)
end
end
end
describe "DELETE #destroy", if: controller_has_action?(:destroy) do
it "requires login" do
logout
delete :destroy, id: record
expect(response).to require_login_web
end
it "enforces authorization" do
delete :destroy, id: record
expect(controller).to enforce_authorization
end
it "deletes the #{resource_singular}" do
# Force record to be created before the `expect` block.
# Otherwise, it is both created and deleted INSIDE the block, causing the
# count not to change.
record
expect{
delete :destroy, id: record
}.to change(model, :count).by(-1)
end
it "redirects to #{delete_redirect_path_helper}" do
delete :destroy, id: record
expect(response).to redirect_to(eval(delete_redirect_path_helper))
end
end
end
答案 2 :(得分:0)
对于let
块,如果将模型作为参数传递给共享示例,就像使用redirect_path_helper一样,它是否不起作用?
include_examples "GET #index", Quote
然后在您的shared_example中,您可以使用record_name
方法从FactoryGirl创建record
和records
并生成valid_attributes和invalid_attributes(您可以创建:invalid_quote
工厂作为很好的无效属性,不确定那些是否被认为是一个很好的做法/想法与FactoryGirl)。
对于第二个问题,您不需要使用指定的路线助手,url_for(controller: :quote)
和url_for(@quote)
都应该有效。