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

Posted

技术标签:

【中文标题】如何从脚手架完成 rspec put 控制器测试【英文标题】:How to complete the rspec put controller test from scaffold 【发布时间】:2014-09-01 04:33:50 【问题描述】:

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

【问题讨论】:

关于 BigDecimal 等式问题,您使用的是什么数据库?您是否尝试过检查 BigDecimal 值的所有 27 位数字? 正如我在下面解释的,BigDecimal 问题是一个红鲱鱼;实际问题出在日期上。 【参考方案1】:

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

让我们以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!会在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、helpers 和自定义匹配器时,您可以保持您的规范超级干燥。一旦你开始在你的控制器规范中重复同样的事情,你就可以找到如何重构它。一开始可能需要一些时间,但完成后,您可以在几分钟内为整个控制器编写测试


最后一句话(我停不下来,我喜欢 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】:

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

tldr;试图确认从 PUT 中返回的每个重要属性都没有改变,这太麻烦了。只需检查更改后的属性是否符合您的预期。

我遇到的问题:

    FactoryGirl.attributes_for 不会返回所有值,因此FactoryGirl: attributes_for not giving me associated attributes 建议使用(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 方法,可以放在 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 动词应该是添加新东西,对吧?如果您最初添加的内容 (new_attributes) 恰好存在于同一模型中的 put 之后,那么您正在测试的内容。

【讨论】:

您的示例与我的第一个示例一样,验证了一个属性已正确更新。但是,它不会验证所有其他属性是否未更改。我实际上是在用一个新实例替换整个工厂实例。【参考方案4】:

这段代码可以用来解决你的两个问题:

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】:

这是我测试 PUT 的方式。那是我notes_controller_spec的一个sn-p,主要思想应该很清楚(如果没有,请告诉我):

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.attributes_for(:company),而不是FactoryGirl.build(:company).attributes.symbolize_keys。它更短,仅包含您在工厂中指定的参数。


很遗憾,对于您的问题,我只能说这些。


附:虽然如果您通过编写类似的样式在数据库层上放置 BigDecimal 相等性检查

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

这可能对你有用。

【讨论】:

【参考方案6】:

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

现在生成器创建的所有示例都将传递给这个控制器规范

【讨论】:

以上是关于如何从脚手架完成 rspec put 控制器测试的主要内容,如果未能解决你的问题,请参考以下文章

Rspec,Rails:如何测试控制器的私有方法?

使用Rails Rspec进行测试 - 预期和得到的不匹配

Rspec:测试所有控制器动作

使用 Javascript 进行 Rspec 控制器测试

如何使用 RSpec 测试 ActionText?

如何使用rspec编写测试用例来发送通知消息