如何从脚手架完成rspec put控制器测试
我正在使用脚手架来生成rspec控制器测试。 默认情况下,它会将测试创建为:
let(:valid_attributes) { skip("Add a hash of attributes valid for your model") } describe "PUT update" do describe "with valid params" do let(:new_attributes) { skip("Add a hash of attributes valid for your model") } it "updates the requested doctor" do company = Company.create! valid_attributes put :update, {:id => company.to_param, :company => new_attributes}, valid_session company.reload skip("Add assertions for updated state") end
使用FactoryGirl,我已经填写了:
let(:valid_attributes) { FactoryGirl.build(:company).attributes.symbolize_keys } describe "PUT update" do describe "with valid params" do let(:new_attributes) { FactoryGirl.build(:company, name: 'New Name').attributes.symbolize_keys } it "updates the requested company", focus: true do company = Company.create! valid_attributes put :update, {:id => company.to_param, :company => new_attributes}, valid_session company.reload expect(assigns(:company).attributes.symbolize_keys[:name]).to eq(new_attributes[:name])
这有效,但似乎我应该能够测试所有属性,而不仅仅是测试更改的名称。 我尝试将最后一行更改为:
class Hash def delete_mutable_attributes self.delete_if { |k, v| %w[id created_at updated_at].member?(k) } end end expect(assigns(:company).attributes.delete_mutable_attributes.symbolize_keys).to eq(new_attributes)
这几乎可以工作,但我从rspec与BigDecimal字段有关的错误:
-:latitude => #, -:longitude => #, +:latitude => #, +:longitude => #,
使用rspec,factory_girl和scaffolding是非常常见的,所以我的问题是:
对于具有有效参数的PUT更新,rspec和factory_girl测试的一个很好的例子是什么? 是否有必要使用attributes.symbolize_keys
并删除可变键? 如何将这些BigDecimal对象作为eq
进行评估?
好的,这就是我的方式,我不会假装严格遵循最佳实践,但我会专注于测试的精确性,代码的清晰度以及我的套件的快速执行。
让我们举一个UserController
例子
1-我不使用FactoryGirl来定义要发布到我的控制器的属性,因为我想要保持对这些属性的控制。 FactoryGirl对于创建记录很有用,但是您总是应该手动设置您正在测试的操作中涉及的数据,这对于可读性和一致性更好。
在这方面,我们将手动定义已发布的属性
let(:valid_update_attributes) { {first_name: 'updated_first_name', last_name: 'updated_last_name'} }
2-然后我定义了我对更新记录的期望属性,它可以是已发布属性的精确副本,但可能是控制器做了一些额外的工作,我们也想测试它。 因此,我们举例说,一旦我们的用户更新了他的个人信息,我们的控制器就会自动添加一个need_admin_validation
标志
let(:expected_update_attributes) { valid_update_attributes.merge(need_admin_validation: true) }
这也是你可以为必须保持不变的属性添加断言的地方。 字段age
示例,但它可以是任何东西
let(:expected_update_attributes) { valid_update_attributes.merge(age: 25, need_admin_validation: true) }
3-我在let
块中定义了动作。 与之前的2一起let
我发现它使我的规格非常易读。 它还可以轻松编写shared_examples
let(:action) { patch :update, format: :js, id: record.id, user: valid_update_attributes }
4- (从那时起,一切都在我的项目中的共享示例和自定义rspec匹配器)创建原始记录的时间,为此我们可以使用FactoryGirl
let!(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 }
如您所见,我们手动设置age
的值,因为我们想要validation它在update
操作期间没有更改。 此外,即使工厂已将年龄设置为25,我也总是覆盖它,因此如果我更换工厂,我的测试不会中断。
第二点需要注意:这里我们使用let!
一声巨响。 那是因为有时您可能想要测试控制器的失败操作,最好的方法是存根valid?
并返回false。 你的存根valid?
你不能再为同一个类创建记录了,因此let!
有爆炸会在存根valid?
之前创建记录valid?
5-断言本身(最后是你问题的答案)
before { action } it { assert_record_values record.reload, expected_update_attributes is_expected.to redirect_to(record) expect(controller.notice).to eq('User was successfully updated.') }
总结所以添加以上所有内容,这就是规范的样子
describe 'PATCH update' do let(:valid_update_attributes) { {first_name: 'updated_first_name', last_name: 'updated_last_name'} } let(:expected_update_attributes) { valid_update_attributes.merge(age: 25, need_admin_validation: true) } let(:action) { patch :update, format: :js, id: record.id, user: valid_update_attributes } let(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 } before { action } it { assert_record_values record.reload, expected_update_attributes is_expected.to redirect_to(record) expect(controller.notice).to eq('User was successfully updated.') } end
assert_record_values
是使rspec更简单的助手。
def assert_record_values(record, values) values.each do |field, value| record_value = record.send field record_value = record_value.to_s if (record_value.is_a? BigDecimal and value.is_a? String) or (record_value.is_a? Date and value.is_a? String) expect(record_value).to eq(value) end end
正如你对BigDecimal
所期望的那样,你可以看到这个简单的帮助器,我们可以写下面的内容,帮助器完成其余的工作
let(:expected_update_attributes) { {latitude: '0.8137713195'} }
所以最后,总结一下,当你编写了shared_examples,helper和自定义匹配器时,你可以保持你的规格超级DRY。 一旦你开始在你的控制器规格中重复相同的事情,你就会发现如何重构它。 一开始可能需要一些时间,但完成后,您可以在几分钟内为整个控制器编写测试
最后一句话(我不能停止,我喜欢Rspec)这里是我的完整助手的样子。 它实际上可用于任何事物,而不仅仅是模型。
def assert_records_values(records, values) expect(records.length).to eq(values.count), "Expected <#{values.count}> number of records, got <#{records.count}>\n\nRecords:\n#{records.to_a}" records.each_with_index do |record, index| assert_record_values record, values[index], index: index end end def assert_record_values(record, values, index: nil) values.each do |field, value| record_value = [field].flatten.inject(record) { |object, method| object.try :send, method } record_value = record_value.to_s if (record_value.is_a? BigDecimal and value.is_a? String) or (record_value.is_a? Date and value.is_a? String) expect_string_or_regexp record_value, value, "#{"(index #{index}) " if index}<#{field}> value expected to be <#{value.inspect}>. Got <#{record_value.inspect}>" end end def expect_string_or_regexp(value, expected, message = nil) if expected.is_a? String expect(value).to eq(expected), message else expect(value).to match(expected), message end end
这是提问者的post。 我不得不在这里了解多个重叠的问题,所以我只想报告我找到的解决方案。
tldr; 尝试确认每个重要属性从PUT恢复不变是很麻烦的。 只需检查更改的属性是否符合预期。
我遇到的问题:
- FactoryGirl.attributes_for不返回所有值,因此FactoryGirl:attributes_for没有给我相关属性建议使用
(Factory.build :company).attributes.symbolize_keys
,它最终会产生新问题。 - 具体来说,Rails 4.1枚举显示为整数而不是枚举值,如下所示: https : //github.com/thoughtbot/factory_girl/issues/680
- 事实certificate,BigDecimal问题是一个红色鲱鱼,由rspec匹配器中的一个错误导致产生不正确的差异。 这是在这里建立的: https : //github.com/rspec/rspec-core/issues/1649
- 实际的匹配器失败是由不匹配的Date值引起的。 这是由于返回的时间不同,但它没有显示,因为
Date.inspect
没有显示毫秒。 - 我用猴子修补的哈希方法解决了这些问题,这种方法象征着键和字符串值。
这是Hash方法,可以在rails_spec.rb中进行:
class Hash def symbolize_and_stringify Hash[ self .delete_if { |k, v| %w[id created_at updated_at].member?(k) } .map { |k, v| [k.to_sym, v.to_s] } ] end end
或者(也许最好)我可以编写一个自定义的rspec匹配器,而不是遍历每个属性并单独比较它们的值,这可能会解决日期问题。 这是我在@Benjamin_Sinclaire选择的答案底部的assert_records_values
方法的方法(对此,谢谢)。
但是,我决定回到更简单的方法来坚持使用attributes_for
,只是比较我改变的属性。 特别:
let(:valid_attributes) { FactoryGirl.attributes_for(:company) } let(:valid_session) { {} } describe "PUT update" do describe "with valid params" do let(:new_attributes) { FactoryGirl.attributes_for(:company, name: 'New Name') } it "updates the requested company" do company = Company.create! valid_attributes put :update, {:id => company.to_param, :company => new_attributes}, valid_session company.reload expect(assigns(:company).attributes['name']).to match(new_attributes[:name]) end
我希望这篇文章允许其他人避免重复我的调查。
好吧,我做了一些非常简单的事情,我正在使用Fabricator,但我很确定它和FactoryGirl一样:
let(:new_attributes) ( { "phone" => 87276251 } ) it "updates the requested patient" do patient = Fabricate :patient put :update, id: patient.to_param, patient: new_attributes patient.reload # skip("Add assertions for updated state") expect(patient.attributes).to include( { "phone" => 87276251 } ) end
另外,我不确定你为什么要建造一个新工厂,PUT动词应该添加新的东西,对吗? 如果您首先添加的内容( new_attributes
)恰好在put
相同模型后存在,那么您正在测试的是什么。
此代码可用于解决您的两个问题:
it "updates the requested patient" do patient = Patient.create! valid_attributes patient_before = JSON.parse(patient.to_json).symbolize_keys put :update, { :id => patient.to_param, :patient => new_attributes }, valid_session patient.reload patient_after = JSON.parse(patient.to_json).symbolize_keys patient_after.delete(:updated_at) patient_after.keys.each do |attribute_name| if new_attributes.keys.include? attribute_name # expect updated attributes to have changed: expect(patient_after[attribute_name]).to eq new_attributes[attribute_name].to_s else # expect non-updated attributes to not have changed: expect(patient_after[attribute_name]).to eq patient_before[attribute_name] end end end
它通过使用JSON将值转换为字符串表示来解决比较浮点数的问题。
它还解决了检查新值是否已更新但其余属性未更改的问题。
但是,根据我的经验,随着复杂性的增加,通常要做的是检查一些特定的对象状态而不是“期望我不更新的属性不会改变”。 想象一下,例如,在控制器中完成更新后,其他一些属性会发生变化,例如“剩余项目”,“某些状态属性”……您想要检查特定的预期更改,这些更改可能超过更新属性。
这是我测试PUT的方法。 这是我的notes_controller_spec
一个片段,主要想法应该是明确的(如果没有,请告诉我):
RSpec.describe NotesController, :type => :controller do let(:note) { FactoryGirl.create(:note) } let(:valid_note_params) { FactoryGirl.attributes_for(:note) } let(:request_params) { {} } ... describe "PUT 'update'" do subject { put 'update', request_params } before(:each) { request_params[:id] = note.id } context 'with valid note params' do before(:each) { request_params[:note] = valid_note_params } it 'updates the note in database' do expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1) end end end end
而不是FactoryGirl.build(:company).attributes.symbolize_keys
,我会写FactoryGirl.attributes_for(:company)
。 它更短,仅包含您在工厂中指定的参数。
不幸的是,我可以就你的问题说些什么。
PS虽然如果你通过写样式在数据库层上放置BigDecimal等式检查
expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1)
这可能对你有用。
使用rspec-rails gem测试rails应用程序。 创建了用户的脚手架。 现在,您需要传递user_controller_spec.rb的所有示例
这已经由脚手架发电机编写。 刚实施
let(:valid_attributes){ hash_of_your_attributes} .. like below let(:valid_attributes) {{ first_name: "Virender", last_name: "Sehwag", gender: "Male"} }
现在将从该文件中传递许多示例。
对于invalid_attributes,请务必在任何字段上添加validation
let(:invalid_attributes) {{first_name: "br"} }
在users模型中,first_name的validation为=>
validates :first_name, length: {minimum: 5}, allow_blank: true
现在,生成器创建的所有示例都将为此controller_spec传递