web-dev-qa-db-ja.com

(ActiveRecord)オブジェクトの等価性をテストする方法

Ruby 1.9.2 オン Rails 3.0.3、2つのFriendの間のオブジェクトの等価性をテストしようとしています(クラスはActiveRecord::Base)オブジェクト。

オブジェクトは同等ですが、テストは失敗します。

Failure/Error: Friend.new(name: 'Bob').should eql(Friend.new(name: 'Bob'))

expected #<Friend id: nil, event_id: nil, name: 'Bob', created_at: nil, updated_at: nil>
     got #<Friend id: nil, event_id: nil, name: 'Bob', created_at: nil, updated_at: nil>

(compared using eql?)

にやにや笑いのためだけに、オブジェクトIDのテストも行いますが、期待どおりに失敗します。

Failure/Error: Friend.new(name: 'Bob').should equal(Friend.new(name: 'Bob'))

expected #<Friend:2190028040> => #<Friend id: nil, event_id: nil, name: 'Bob', created_at: nil, updated_at: nil>
     got #<Friend:2190195380> => #<Friend id: nil, event_id: nil, name: 'Bob', created_at: nil, updated_at: nil>

Compared using equal?, which compares object identity,
but expected and actual are not the same object. Use
'actual.should == expected' if you don't care about
object identity in this example.

誰かがオブジェクトの同等性の最初のテストが失敗する理由と、これら2つのオブジェクトが同等であると正常にアサートする方法を説明できますか?

49

Railsは、ID列に同等性チェックを意図的に委任します。 2つのARオブジェクトに同じものが含まれているかどうかを知りたい場合は、両方で#attributesを呼び出した結果を比較してください。

44
noodl

==eql?(エイリアスActiveRecord::Base)操作の APIドキュメント をご覧ください

Comparison_objectが正確に同じオブジェクトである場合、またはcomparison_objectが同じタイプでselfがIDを持ち、comparation_object.idと等しい場合にtrueを返します。

新しいレコードは、他のレコードが受信者自身でない限り、定義により他のレコードとは異なることに注意してください。また、selectを使用して既存のレコードを取得し、IDを省略した場合、自分で操作すると、この述語はfalseを返します。

また、レコードを破棄すると、モデルインスタンスのIDが保持されるため、削除されたモデルは依然として比較可能です。

40
Andy Lindeman

属性に基づいて2つのモデルインスタンスを比較する場合は、おそらくexcludeのような比較から特定の無関係な属性(id、_created_at_など)を使用します。 _updated_at_。 (レコードのデータ自体の一部よりもmetadataであると考えます。)

これは、2つの新しい(保存されていない)レコードを比較する場合は関係ないかもしれません(id、_created_at_、および_updated_at_はすべて保存されるまでnilであるため) savedオブジェクトとnsaved oneを比較する必要がある場合があります(この場合==はnil!= 5なのでfalseになります)。または、2つのsavedオブジェクトを比較して、同じdataが含まれているかどうかを調べたい(したがって、falseを返すため、ActiveRecord _==_演算子は機能しません)それらが異なるidを持っている場合、それ以外は同一であっても)。

この問題に対する私の解決策は、属性を使用して比較したいモデルに次のようなものを追加することです。

_  def self.attributes_to_ignore_when_comparing
    [:id, :created_at, :updated_at]
  end

  def identical?(other)
    self. attributes.except(*self.class.attributes_to_ignore_when_comparing.map(&:to_s)) ==
    other.attributes.except(*self.class.attributes_to_ignore_when_comparing.map(&:to_s))
  end
_

それから私の仕様では、このように読みやすく簡潔なものを書くことができます:

_Address.last.should be_identical(Address.new({city: 'City', country: 'USA'}))
_

_active_record_attributes_equality_ gemをフォークし、この動作を使用するように変更して、より簡単に再利用できるようにすることを計画しています。

ただし、次のような質問があります。

  • そのような宝石はすでに存在しますか??
  • メソッドは何と呼ばれますか?既存の_==_演算子をオーバーライドすることは良い考えだとは思わないので、今のところは_identical?_と呼んでいます。しかし、おそらく_practically_identical?_や_attributes_eql?_のようなものは、それらがstrictly同一かどうかをチェックしていないため、より正確になるでしょう。(some属性は異なることが許されます。)...
  • _attributes_to_ignore_when_comparing_は冗長すぎます。 gemのデフォルトを使用する場合、これを各モデルに明示的に追加する必要はありません。 _ignore_for_attributes_eql :last_signed_in_at, :updated_at_のようなクラスマクロでデフォルトを上書きできるようにするかもしれません

コメントは大歓迎です...

Update:_active_record_attributes_equality_をフォークする代わりに、まったく新しいgemactive_record_ignored_attributeshttp://github.com/TylerRick/active_record_ignored_attributes および http://rubygems.org/gems/active_record_ignored_attributes で利用可能

20
Tyler Rick
 META = [:id, :created_at, :updated_at, :interacted_at, :confirmed_at]

 def eql_attributes?(original,new)
   original = original.attributes.with_indifferent_access.except(*META)
   new = new.attributes.symbolize_keys.with_indifferent_access.except(*META)
   original == new
 end

 eql_attributes? attrs, attrs2
2
scott

このタイプの比較のために、RSpecでマッチャーを作成しました。非常にシンプルですが、効果的です。

このファイル内:spec/support/matchers.rb

このマッチャーを実装できます...

RSpec::Matchers.define :be_a_clone_of do |model1|
  match do |model2|
    ignored_columns = %w[id created_at updated_at]
    model1.attributes.except(*ignored_columns) == model2.attributes.except(*ignored_columns)
  end
end

その後、次の方法で仕様を作成するときに使用できます...

item = create(:item) # FactoryBot gem
item2 = item.dup

expect(item).to be_a_clone_of(item2)
# True

便利なリンク:

https://relishapp.com/rspec/rspec-expectations/v/2-4/docs/custom-matchers/define-matcherhttps://github.com/thoughtbot/ factory_bot

2
Victor