代码之家  ›  专栏  ›  技术社区  ›  Dan Kohn

如何在脚手架上完成rspec放置控制器测试

  •  31
  • Dan Kohn  · 技术社区  · 10 年前

    我正在使用脚手架生成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 ?

    6 回复  |  直到 10 年前
        1
  •  38
  •   Benjamin Bouchet    10 年前

    好的,这就是我的做法,我并不假装严格遵循最佳实践,但我专注于测试的准确性、代码的清晰性和套件的快速执行。

    因此,让我们以 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个 允许 我发现它使我的规范非常可读。而且它还使编写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 }
    

    如您所见,我们手动设置 年龄 因为我们想验证它在 update 行动此外,即使工厂已经将年龄设置为25岁,我也会覆盖它,这样如果我更改工厂,我的测试就不会中断。

    第二点需要注意:这里我们使用 let! 砰的一声。这是因为有时您可能想测试控制器的失败操作,而最好的方法是存根 valid? 并返回false。一旦你截住了 有效的 因此,您不能再为同一类创建记录 允许 一声巨响就能创造纪录 之前 的存根 有效的

    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'} }
    

    最后,总结一下,当您编写了共享示例、助手和自定义匹配器时,您可以保持规范的超干燥。一旦您开始在控制器规范中重复相同的操作,就可以找到如何重构它。开始可能需要时间,但完成后,您可以在几分钟内为整个控制器编写测试


    最后一句话(我停不下来,我喜欢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
    
        2
  •  8
  •   Community dbr    7 年前

    这是提问者的帖子。我必须深入了解这里的多个重叠问题,所以我只想报告一下我找到的解决方案。

    太长,读不下去了试图确认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. 我用一个猴子补丁的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
    

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

        3
  •  3
  •   Pablo Olmos de Aguilera C.    10 年前

    嗯,我做了一件非常简单的事情,我使用的是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 在同一模型中。

        4
  •  2
  •   chipairon    10 年前

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

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

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

    然而,根据我的经验,随着复杂性的增加,通常要做的是检查一些特定的对象状态,而不是“期望我不更新的属性不会改变”。例如,想象一下,当在控制器中进行更新时,其他一些属性会发生变化,比如“剩余项目”、“一些状态属性”。。。您希望检查特定的预期更改,这可能不仅仅是更新的属性。

        5
  •  1
  •   nsave    10 年前

    这是我测试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)
    

    这可能对你有用。

        6
  •  1
  •   Navnath    7 年前

    使用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