如何从脚手架完成rspec put控制器测试

Dan*_*ohn 28 rspec ruby-on-rails scaffolding rspec-rails factory-bot

我正在使用脚手架来生成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
Run Code Online (Sandbox Code Playgroud)

使用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])
Run Code Online (Sandbox Code Playgroud)

这有效,但似乎我应该能够测试所有属性,而不仅仅是测试更改的名称.我尝试将最后一行更改为:

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)
Run Code Online (Sandbox Code Playgroud)

这几乎可以工作,但我从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)>,
Run Code Online (Sandbox Code Playgroud)

使用rspec,factory_girl和scaffolding是非常常见的,所以我的问题是:

对于具有有效参数的PUT更新,rspec和factory_girl测试的一个很好的例子是什么?是否有必要使用attributes.symbolize_keys和删除可变密钥?如何将这些BigDecimal对象评估为eq?

Ben*_*enj 32

好的,这就是我的工作方式,我并没有假装严格遵循最佳实践,但我专注于测试的精确性,代码的清晰度以及我的套件的快速执行.

让我们举个例子 UserController

1-我不使用FactoryGirl定义要发布到我的控制器的属性,因为我想保持对这些属性的控制.FactoryGirl对于创建记录很有用,但是您总是应该手动设置您正在测试的操作中涉及的数据,这对于可读性和一致性更好.

在这方面,我们将手动定义发布的属性

let(:valid_update_attributes) { {first_name: 'updated_first_name', last_name: 'updated_last_name'} }
Run Code Online (Sandbox Code Playgroud)

2-然后我定义了我对更新记录的期望属性,它可以是已发布属性的精确副本,但可能是控制器做了一些额外的工作,我们也想测试它.因此,我们举例说,一旦我们的用户更新了他的个人信息,我们的控制器就会自动添加一个need_admin_validation标志

let(:expected_update_attributes) { valid_update_attributes.merge(need_admin_validation: true) }
Run Code Online (Sandbox Code Playgroud)

这也是你可以为必须保持不变的属性添加断言的地方.该字段的示例age,但它可以是任何东西

let(:expected_update_attributes) { valid_update_attributes.merge(age: 25, need_admin_validation: true) }
Run Code Online (Sandbox Code Playgroud)

3-我在let块中定义动作.与之前的2一起,let我发现它使我的规格非常易读.它还可以轻松编写shared_examples

let(:action) { patch :update, format: :js, id: record.id, user: valid_update_attributes }
Run Code Online (Sandbox Code Playgroud)

4-(从那时起,一切都在我的项目中的共享示例和自定义rspec匹配器)创建原始记录的时间,为此我们可以使用FactoryGirl

let!(:record) { FactoryGirl.create :user, :with_our_custom_traits, age: 25 }
Run Code Online (Sandbox Code Playgroud)

如您所见,我们手动设置值,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.')
}
Run Code Online (Sandbox Code Playgroud)

总结所以添加以上所有内容,这就是规范的样子

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

正如您在我们期望的时候可以看到这个简单的帮助器BigDecimal,我们可以编写以下内容,帮助器完成剩下的工作

let(:expected_update_attributes) { {latitude: '0.8137713195'} }
Run Code Online (Sandbox Code Playgroud)

最后,最后,当您编写了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}>\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
Run Code Online (Sandbox Code Playgroud)


Dan*_*ohn 6

这是提问者的帖子.我不得不在这里了解多个重叠的问题,所以我只想报告我找到的解决方案.

tldr; 尝试确认每个重要属性从PUT恢复不变是很麻烦的.只需检查更改的属性是否符合预期.

我遇到的问题:

  1. FactoryGirl.attributes_for不返回所有值,因此FactoryGirl:attributes_for不给我相关属性建议使用(Factory.build :company).attributes.symbolize_keys,这最终会产生新问题.
  2. 具体来说,Rails 4.1枚举显示为整数而不是枚举值,如下所示:https://github.com/thoughtbot/factory_girl/issues/680
  3. 事实证明,BigDecimal问题是一个红色鲱鱼,由rspec匹配器中的一个错误导致产生不正确的差异.这是在这里建立的:https://github.com/rspec/rspec-core/issues/1649
  4. 实际的匹配器失败是由不匹配的Date值引起的.这是由于返回的时间不同,但它没有显示,因为Date.inspect没有显示毫秒.
  5. 我用猴子修补的Hash方法解决了这些问题,这种方法象征着键和stringifes值.

这是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
Run Code Online (Sandbox Code Playgroud)

或者(也许最好)我可以写一个自定义的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
Run Code Online (Sandbox Code Playgroud)

我希望这篇文章允许其他人避免重复我的调查.