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つのオブジェクトが同等であると正常にアサートする方法を説明できますか?
Railsは、ID列に同等性チェックを意図的に委任します。 2つのARオブジェクトに同じものが含まれているかどうかを知りたい場合は、両方で#attributesを呼び出した結果を比較してください。
==
のeql?
(エイリアスActiveRecord::Base
)操作の APIドキュメント をご覧ください
Comparison_objectが正確に同じオブジェクトである場合、またはcomparison_objectが同じタイプでselfがIDを持ち、comparation_object.idと等しい場合にtrueを返します。
新しいレコードは、他のレコードが受信者自身でない限り、定義により他のレコードとは異なることに注意してください。また、selectを使用して既存のレコードを取得し、IDを省略した場合、自分で操作すると、この述語はfalseを返します。
また、レコードを破棄すると、モデルインスタンスのIDが保持されるため、削除されたモデルは依然として比較可能です。
属性に基づいて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_attributes、 http://github.com/TylerRick/active_record_ignored_attributes および http://rubygems.org/gems/active_record_ignored_attributes で利用可能
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
このタイプの比較のために、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