5

I want to check if no attributes on an ActiveRecord object have been modified. Currently I'm doing this:

prev_attr = obj.attributes <- this will give me back a Hash with attr name and attr value

And then, later, I grab the attributes again and compare the 2 hashes. Is there another way?

Geo
  • 93,257
  • 117
  • 344
  • 520

4 Answers4

11

There is another way indeed. You can do it like this :

it "should not change sth" do
  expect {
    # some action
  }.to_not change{subject.attribute}
end

See https://www.relishapp.com/rspec/rspec-expectations/v/2-0/docs/matchers/expect-change.

tomferon
  • 4,993
  • 1
  • 23
  • 44
  • But, what if I have multiple attributes? – Geo May 07 '12 at 12:40
  • 1
    I guess `change{%w{name title description}.map { |attr| subject.send(attr) }}` will work fine as it will generate an array of the attributes' value which RSpec should be able to compare correctly. – tomferon May 07 '12 at 13:11
  • Bit concerned about the maintainability of this - what if I add a new attribute to the model? ... And another? I would need to keep returning to this test to update it. – Rob Cooper May 09 '12 at 08:57
  • You can test the subject, not just the attribute(s) `expect{foo}.not_to change{my_object}` – Sam Jul 17 '18 at 12:11
3

You can use ActiveRecord::Dirty. It gives you a changed? method on your model which is truthy if any attribute of the model was actually changed and falsey if not. You also have _changed? methods for each attribute, e.g. model.subject_changed? which is truthy if that attribute was changed compared to when the object was read from the database.

To compare attribute values, you can use model.subject_was which will be the original value the attribute had when the object was instantiated. Or you can use model.changes which will return a hash with the attribute name as the key and a 2-element array containing the original value and the changed value for each changed attribute.

Holger Just
  • 52,918
  • 14
  • 115
  • 123
  • 2
    Keep in mind that once the changes are saved, `changed?` will return `false`. So the test could pass under scenarios where in fact it has been changed. – Rob Cooper May 09 '12 at 08:26
  • Of course, but unless you test deeply nested code, it's often a good idea to not save the model at all, if only to conserve performance of the test suite. Given that, using AR::Dirty is still the easiest way to check an in-flight model instance, even more so outside of rspec (and I thus don't think this answer deserves a downvote). – Holger Just May 09 '12 at 08:32
  • It's not about testing deeply nested code of should/should not save models. It's about the test providing a consistent, repeatable result. Using `AR::Dirty` has clear scenarios where the internals of the code under test could cause it to return a false positive. Therefore, I think it shouldn't be used. – Rob Cooper May 09 '12 at 08:49
  • I'm all for "easiest way", hence my answer simply using equality matchers (which would be consistent). – Rob Cooper May 09 '12 at 08:52
2

You should just be able to use equality matchers - does this not work for you?

a = { :test => "a" }
b = { :test => "b" }
$ a == b
=> false
b = { :test => "a" }
$ a == b
=> true

Or to use your example:

original_attributes = obj.attributes
# do something that should *not* manipulate obj
new_attributes = obj.attributes
new_attributes.should eql original_attributes
Rob Cooper
  • 28,567
  • 26
  • 103
  • 142
1

Not a perfect solution, but as is mentioned here, I prefer it due to maintainability.

The attributes are cached, and since they're not changed directly you'll have to reload them if you want to check them all at once:

it 'does not change the subject attributes' do
  expect {
    # Action
  }.to_not change { subject.reload.attributes }
end

Avoid reloading if you can, since you're forcing another request to the database.

Gabriel Osorio
  • 1,003
  • 9
  • 8