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

时间:2014-07-10 17:21:53

标签: ruby-on-rails rspec factory-bot rspec-rails scaffolding

我使用脚手架来生成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和scaffolding是非常常见的,所以我的问题是:

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

6 个答案:

答案 0 :(得分:30)

好的,这就是我的方式,我不会假装严格遵循最佳实践,但我会专注于测试的准确性,代码的清晰度以及我的套件的快速执行。

让我们举一个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的值,因为我们要验证它在update操作期间没有更改。此外,即使工厂已经将年龄设置为25,我也总是覆盖它,所以如果我改变工厂,我的测试就不会中断。

要注意的第二件事:我们在这里使用let!。这是因为有时您可能想要测试控制器的失败操作,最好的方法是存根valid?并返回false。存储valid?后,您无法再为同一个类创建记录,因此let!带有爆炸将在 {{1}的存根之前创建记录 }

5 - 断言本身(最后是你问题的答案)

valid?

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

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 是帮助您简化rspec的助手。

assert_record_values

正如您对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

最后,最后,当您编写了shared_examples,帮助程序和自定义匹配器时,您可以保持您的规格超级DRY。一旦你开始在你的控制器规范中重复相同的事情,你会发现如何重构它。一开始可能需要一些时间,但是一旦完成,您可以在几分钟内为整个控制器编写测试


最后一句话(我不能停止,我喜欢Rspec)这里是我的完整助手的样子。它实际上可用于任何事物,而不仅仅是模型。

let(:expected_update_attributes) { {latitude: '0.8137713195'} }

答案 1 :(得分:5)

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

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

我遇到的问题:

  1. FactoryGirl.attributes_for不会返回所有值,因此FactoryGirl: attributes_for not giving me associated attributes建议使用(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.inspect没有显示毫秒。
  5. 我用猴子修补的哈希方法解决了这些问题,这种方法象征着键和字符串值。
  6. 这里是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
    

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

答案 2 :(得分:3)

嗯,我做了一些非常简单的事情,我正在使用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之后,那么您正在测试的内容。

答案 3 :(得分:2)

此代码可用于解决您的两个问题:

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将值转换为字符串表示来解决比较浮点数的问题。

它还解决了检查新值是否已更新但其余属性未更改的问题。

根据我的经验,随着复杂性的增加,通常要做的是检查一些特定的对象状态而不是&#34;期望我不会更新的属性不会改变&# 34 ;.例如,想象一下,随着更新在控制器中完成,其他一些属性会发生变化,例如&#34;其余项目&#34;,&#34;某些状态属性&#34; ...您想要检查具体的预期的更改,可能超过更新的属性。

答案 4 :(得分:1)

这是我测试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)。它更短,仅包含您在工厂中指定的参数。


不幸的是,我可以就你的问题说些什么。


P.S。虽然如果您通过写入

这样的样式来对数据库层进行BigDecimal等式检查
expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1)

这可能适合你。

答案 5 :(得分: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的验证为as =&gt;

  validates :first_name, length: {minimum: 5}, allow_blank: true

现在,生成器创建的所有示例都将传递给此controller_spec