如何完成来自脚手架的rspec put控制器测试

31

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


关于BigDecimal相等性问题,您使用的是哪个数据库?您是否尝试检查您的BigDecimal值的所有27位数字? - Peter Alfvin
正如我下面所解释的那样,BigDecimal问题是一个误导;实际问题出在日期上。 - Dan Kohn
6个回答

38

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

UserController 为例:

1- 我不使用 FactoryGirl 来定义要发送到我的控制器的属性,因为我想保持对这些属性的控制。FactoryGirl 用于创建记录,但你始终应该手动设置涉及到正在测试的操作中的数据,这样更易读和一致。

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

let(:valid_update_attributes) { {first_name: 'updated_first_name', last_name: 'updated_last_name'} }

第二步- 接下来我定义我期望更新记录所具有的属性,这些属性可以是与发布的属性完全相同的副本,但控制器还可以执行一些额外的操作,我们也想测试一下。因此,我们假设对于我们的示例,一旦用户更新了个人信息,我们的控制器会自动添加一个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块中定义动作。与前面的两个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!将在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

使用这个简单的helper,当我们期望一个BigDecimal时,只需要编写以下代码,helper会自动完成剩余部分。

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

最后总结,当你编写完你的shared_examples、helpers和自定义匹配器后,你可以让你的测试用例保持超级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

我是提问者,请参见下面我最终使用的解决方案。 - Dan Kohn

8

我是提问者。为了理解这里的多个重叠问题,我不得不深入研究一下。现在我想向大家报告我找到的解决方案。

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. 我通过一个猴子补丁的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匹配器,遍历每个属性并逐个比较它们的值,这样就可以解决日期问题。这是我选择的答案底部@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

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

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动词应该是添加新内容的对吧?如果您在同一模型中进行PUT之后,检查是否存在于第一次添加的内容(new_attributes),那您到底在测试什么呢?


你的例子,就像我的第一个例子一样,验证了一个属性已经被正确更新。然而,它并没有验证所有其他属性都没有改变。我本质上是用一个新实例替换整个工厂实例。 - Dan Kohn

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

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

但在我看来,随着复杂性的增加,通常要做的是检查某些特定对象状态,而不是“期望我不更新的属性不会改变”。例如,想象一下,在控制器中进行更新时,其他属性发生了一些变化,如“剩余项目”,“某些状态属性”...您希望检查特定的预期更改,可能超过了更新的属性。


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)。它更短且只包含你在工厂中指定的参数。


很遗憾,这就是我能对你的问题说的全部了。


顺便说一下,如果您想在数据库层上进行 BigDecimal 相等性检查,可以按以下方式编写:

expect{ subject }.to change{ Note.where(valid_note_params).count }.by(1)

这可能适用于您。

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

现在将从这个文件中展示许多例子。
对于无效属性,请确保在任何字段上添加验证。
let(:invalid_attributes) {{first_name: "br"}
  }

在用户模型中,对于first_name的验证方式为 =>
  validates :first_name, length: {minimum: 5}, allow_blank: true

现在,由生成器创建的所有示例都将通过此controller_spec。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接