web-dev-qa-db-ja.com

足場からrspec putコントローラーテストを完了する方法

私は足場を使用して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)

それはほとんど機能しましたが、BigDecimalフィールドに関係するrspecから次のエラーが発生します。

   -: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として評価するにはどうすればよいですか?

29
Dan Kohn

わかりました。これが私のやり方です。厳密にベストプラクティスに従うふりはしませんが、テストの精度、コードの明確さ、およびスイートの高速実行に焦点を当てています。

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

-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歳に設定されている場合でも、常に上書きするため、工場を変更してもテストが中断しません。

2番目に注意すること:ここではlet!を強打して使用します。これは、コントローラーの失敗アクションをテストしたい場合があるためで、そのための最善の方法は、valid?をスタブしてfalseを返すことです。 valid?をスタブにすると、同じクラスのレコードを作成できなくなります。そのため、let!を使用すると、beforevalid?のスタブ

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、ヘルパー、カスタムマッチャーを作成したら、仕様を超乾燥状態に保つことができます。コントローラーの仕様で同じことを繰り返し始めるとすぐに、これをリファクタリングする方法を見つけます。最初は時間がかかるかもしれませんが、完了すると数分でコントローラー全体のテストを書くことができます


そして最後の言葉(止められない、私は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
31
Benj

これは質問者投稿です。ここで複数の重複する問題を理解するには、うさぎの穴を少し掘り下げる必要があったので、見つけた解決策について報告したいと思いました。

tldr;すべての重要な属性が変更されずにPUTから戻ってくることを確認しようとするのは、非常に困難です。変更された属性が期待どおりであることを確認してください。

私が遭遇した問題:

  1. FactoryGirl.attributes_forはすべての値を返すわけではないので、 FactoryGirl:attributes_forが関連する属性を与えない(Factory.build :company).attributes.symbolize_keysの使用を提案します。
  2. 具体的には、Rails 4.1 enumは、ここで報告されているように、enum値ではなく整数として表示されます。 https://github.com/thoughtbot/factory_girl/issues/68
  3. BigDecimalの問題は、不適切なdiffを生成するrspecマッチャーのバグが原因で発生したレッドニシンであることがわかりました。これはここで確立されました: https://github.com/rspec/rspec-core/issues/1649
  4. 実際のマッチャーの失敗は、一致しない日付値が原因です。これは、返された時間が異なるためですが、Date.inspectにはミリ秒が表示されないため、表示されません。
  5. キーと文字列の値を記号化するサルのパッチを適用したハッシュメソッドでこれらの問題を回避しました。

Rails_spec.rbに入れることができるHashメソッドは次のとおりです。

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

この投稿により、他の人が私の調査を繰り返さないようにできることを願っています。

5
Dan Kohn

まあ、私はかなり簡単なことをしました。私は製作者を使用していますが、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の後に偶然存在します。

このコードは、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を使用して値を文字列表現に変換することにより、浮動小数点数を比較する問題を解決します。

また、新しい値が更新され、残りの属性が変更されていないことを確認する問題も解決します。

ただし、私の経験では、複雑さが増すにつれて、「更新しない属性が変更されないことを期待する」のではなく、特定のオブジェクトの状態をチェックすることが通常行われます。たとえば、「残りのアイテム」、「一部のステータス属性」など、コントローラで更新が行われると、他のいくつかの属性が変化することを想像してみてください。特定の予想される変更を確認したいのですが、更新されたものよりも多い場合があります。属性。

2
chipairon

Railsアプリケーションをrspec-Rails gemでテストします。ユーザーの足場を作成しました。次に、user_controller_spec.rbのすべての例を渡す必要があります

これはすでにscaffoldジェネレーターによって書かれています。実装するだけ

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に渡されます

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
nsave