rspec-rails: `change` doesn't handle ActiveRecord objects that well
I ran into this today while trying to use the new have_attributes matcher:
widget = create(Widget, name: 'widget', package: 'single')
form_attributes = {
id: widget.to_param,
widget: {
name: 'A new name',
package: 'bulk',
}
}
expect {
put :update, form_attributes, valid_session
}.to change{ Widget.find(widget.id) }.to have_attributes(
name: 'A new name',
package: 'bulk',
)
Running this fails with:
expected result to have changed to have attributes { ... }, but did not change
Root Cause
The root of this issue is this line from the RSpec::Matchers::BuiltIn::ChangeDetails internal implementation used by the change matcher:
@actual_before != @actual_after
It’s attempting to check that the objects aren’t equal, thus not changing. However, ActiveRecord overrides == to mean do these objects have the same id value. If so, then they are the same object. This causes change to fail.
Workarounds
- Call
duponActiveRecordobjects in thechangeblock. This will remove theidresulting inActiveRecordto fall back on a more intensive attribute match. - Call
ActiveRecord#attributesto get the attribute list and use RSpec’s standard hash matchers such asinclude.
About this issue
- Original URL
- State: open
- Created 10 years ago
- Comments: 22 (17 by maintainers)
Commits related to this issue
- draft! Support objects that do not change with a specifier Fixes https://github.com/rspec/rspec-rails/issues/1173 — committed to rspec/rspec-expectations by pirj 4 years ago
I like this idea a lot – and it is pretty easy to implement a working version:
That said, there’s some edge cases that would need to be handled for a version of this offered by RSpec, such as
change_attributes(post, :owner)(vschange_attributes { post.owner }) and if we’re going to call itchange_attributesit should probably handle any object, not just AR models, given that we already havehave_attributeswhich handles any object. The implementation to handle arbitrary objects would have to be more involved (but is doable) or we could call thischange_model_attributesand restrict it to AR model objects.I looked into it and was able to reproduce the issue. See last specs, it is still failing.
I also wrote two workarounds if someone needs help on this.
So the hash for any model is a XOR or class hash and id hash. No matter the attributes, it’s a constant for the same row:
and
==for models is defined as:In the
changematcher, we check:and both those comparisons return
false.It’s not really fair, not only in the context of RSpec Rails:
it’s a pretty contrived example, but still.
So, in my opinion, if
changeis given a qualifier, it should not ignore the qualifier as it does now:if
change_details.changed?evaluates tofalseas it is in the abovementioned cases.I’m going to address this in
rspec-expectations, as this is notrspec-railsspecific.