Rspec-rails: Expect action to change object attribute to something

Created on 8 Jan 2020  ·  5Comments  ·  Source: rspec/rspec-rails

Is your feature request related to a problem? Please describe.
It appears that the most common way to check if an attribute has changed is to check whether that attribute value equals what you expected.
This is quite unclear to me because sometimes you are not sure what the initial value of that attribute was, and you do not want to start checking factories just to confirm that.

Describe the solution you'd like
It would be nice if we had a matcher that checks if an attribute name changed to something, or not otherwise.
For example expect { update_user }.to change(user.reload, :name).to 'Kaka Ruto' and expect { update_user }.not_to change(user.reload, :name).to 'Kaka Ruto'

Describe alternatives you've considered
I have had to check if the value of that attribute equals what I expect. For example
expect(user.reload.name).to eq 'Kaka Ruto'

Additional context
I tried using the existing change matchers but got an unexpected behaviour.
For this test, expect { update_user }.to change(user.reload, :name).to 'Kaka Ruto', I got a failing spec with the description expected 'User#name' to have changed to "Kaka Ruto", but did not change. However, it's negated variant passes! This expect { update_user }.not_to change(user.reload, :name) actually passes

Most helpful comment

To avoid the double reload you can also do:

expect { update_user; user.reload }.to change { user.name }.to 'Kaka Ruto'

All 5 comments

The problem with this code is that reload is sent before the change.
So basically this:

expect { update_user }.to change(user.reload, :name).to 'Kaka Ruto'

expands to:

object = user.reload
old_value = object.send(:name)
update_user
new_value = object.send(:name)
expect(new_value).not_to eq(old_value) # i.e. "change"
expect(new_value).to eq('Kaka Ruto')

There are two problems here. One is that reload is executed before the change. You probably expected it to reload after the change.
And another is that you have to do a reload, something in your code instead of using the model is finding it by id and re-instantiating it from scratch, so the one instantiated in test goes out of sync with the one in the tested code forcing you to reload.

I suggest you to use:

expect { update_user }.to change { user.reload.name }.to 'Kaka Ruto'

This has a downside that reload is executed twice, before the change and after.
I've seen something like change_reloaded(model, attribute_name) matcher that saves the value for comparison before the change, then reloads and compares after the change.
However, the issue that update_user is loading the model from the database still stands.

Please feel free to reopen in case you feel that rspec-rails is incomplete or has some flaw.

Thank you @pirj ! That does help indeed.

You're always welcome!

To avoid the double reload you can also do:

expect { update_user; user.reload }.to change { user.name }.to 'Kaka Ruto'

@JonRowe nifty!

Was this page helpful?
0 / 5 - 0 ratings