我正在使用脚手架来生成 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 => #<BigDecimal:7fe376b430c8,'0.8137713195 830835E2',27(27)>,
-:longitude => #<BigDecimal:7fe376b43078,'-0.1270954650 1027958E3',27(27)>,
+:latitude => #<BigDecimal:7fe3767eadb8,'0.8137713195 830835E2',27(27)>,
+:longitude => #<BigDecimal:7fe3767ead40,'-0.1270954650 1027958E3',27(27)>,
使用 rspec、factory_girl 和脚手架非常普遍,所以我的问题是:
使用有效参数的 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 匹配器中(是时候创建原始记录了,为此我们可以使用 工厂女孩
let!(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 }
如您所见,我们手动设置了age
的值,因为我们想验证它在update
操作期间没有更改。此外,即使工厂已经将年龄设置为 25,我也会覆盖它,这样如果我更改工厂,我的测试就不会中断。
第二件要注意的事情:在这里我们使用带有爆炸声的let!
。这是因为有时您可能想要测试控制器的失败操作,而执行此操作的最佳方法是存根valid?
并返回 false。一旦你存根valid?
你就不能再为同一个类创建记录了,因此,let!
一声巨响,就会在存根之前创建记录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、助手和自定义匹配器时,您可以保持您的规格超级干燥。一旦您开始在控制器规范中重复同样的事情,请找到如何重构它。一开始可能需要一些时间,但是完成后,您可以在几分钟内为整个控制器编写测试
最后一句话(我不能停下来,我喜欢 Rspec(这里是我的全帮手的样子。事实上,它可用于任何东西,而不仅仅是模型。
def assert_records_values(records, values)
expect(records.length).to eq(values.count), "Expected <#{values.count}> number of records, got <#{records.count}>nnRecords: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
这是提问者的帖子。为了理解这里的多个重叠问题,我不得不稍微深入兔子洞,所以我只想报告我找到的解决方案。
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
- 事实证明,BigDecimal 问题是一个红鲱鱼,由 rspec 匹配器中的一个错误引起,该错误会产生不正确的差异。这是在这里建立的:https://github.com/rspec/rspec-core/issues/1649
- 实际匹配器故障是由不匹配的日期值引起的。这是由于返回的时间不同,但它不会显示,因为
Date.inspect
不显示毫秒。 - 我用一个猴子修补的 Hash 方法来解决这些问题,该方法符号化键和字符串值。
下面是 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 匹配器,而不是遍历每个属性并单独比较它们的值,这可以解决日期问题。这就是我选择的答案底部的assert_records_values
方法的方法 @Benjamin_Sinclaire (为此,谢谢(。
但是,我决定回到更简单的方法,坚持使用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)
。它较短,仅包含您在工厂中指定的参数。
不幸的是,这就是我能说的关于你的问题的全部内容。
附言虽然如果你把 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,请务必在任何字段和
let(:invalid_attributes) {{first_name: "br"}
}
在用户模型中,first_name的验证为 =>
validates :first_name, length: {minimum: 5}, allow_blank: true
现在,生成器创建的所有示例都将通过此controller_spec