I have three models:
User
Company
Commitment
Commitment
is a HABTM join table for Users
and Companies
(i.e., when a User
joins a Company
, it creates a new Commitment
). It also has a few extra columns/attributes:
admin
(does the user have admin privileges for this company?)confirmed_by_admin
(has a company admin confirmed this user's request to join the company?)confirmed_by_member
(has the user himself confirmed an invitation to join the company?)I've also defined a convenience method to quickly determine if a commitment is fully confirmed:
class Commitment < ApplicationRecord
def confirmed?
confirmed_by_admin? && confirmed_by_member?
end
end
Now I'm writing a request spec, but for some reason, the change
matcher only works with one of its two syntaxes:
let :carol { FactoryGirl.create(:user) }
let :company { FactoryGirl.create(:company) }
it 'confirms invitation to join company' do
# Initialize unconfirmed commitment
FactoryGirl.create(:commitment, user: carol,
company: company,
confirmed_by_admin: true)
expect do
patch commitment_path(carol.commitments.first),
params: { commitment: { confirmed_by_member: true } }
# for the following syntaxes, ------------------------------------------------
# this works:
end.to change { carol.commitments.first.confirmed?) }.from(false).to(true)
# and this fails:
end.to change(carol.commitments.first, :confirmed?).from(false).to(true)
# ----------------------------------------------------------------------------
end
It appears that carol.commitments.first
isn't being reloaded when RSpec tests for the change — I get the following test output:
Failure/Error:
expect do
patch commitment_path(carol.commitments.first),
params: { commitment: { confirmed_by_member: true } }
end.to change(Commitment.find_by(user: carol, company: company), :confirmed?).from(false).to(true)
expected #confirmed? to have changed from false to true, but did not change
# ./spec/requests/commitments_spec.rb:69:in `block (3 levels) in <top (required)>'
What gives? Clearly I can just stick to the curly-brace / block syntax, but I'd like to understand why one works and not the other.
Upon inspecting docs and trying out myself a new rails project replicating your scenarios, and also failing, I believe that the reason why it was failing is because
the "block" form of .change
is ran twice ("before" and "after" the expect
block), whatever is inside of that block:
.change{ carol.commitments.first.confirmed? }
while the "method" form of .change
is ran once for the first argument: carol.commitments.first
, but ran twice for the second argument :confirmed?
. However, the problem with this is that the the carol.commitments.first
at this point inside the spec file does not share the same memory space as that object that has been actually updated in your commitments_controller#update
(most likely that object is named @commitment
). Although they are the same Commitment
record, they are separate instances, and the attribute-values of the other do not automatically changes when the another one changed.
Consider the following which demonstrates a scenario in which this "method" form works:
it 'sometest' do
commitment = FactoryGirl.create(:commitment, user: carol,
company: company,
confirmed_by_admin: true)
expect do
# this commitment object is exactly the same object passed in the `change` below
commitment.confirmed_by_member = true
end.to change(commitment, :confirmed?).from(false).to(true)
end
Disclaimer: This is unverified, but because it was too complex for me write as a comment (with all the sample test code), I wrote it here as an answer. Should anyone know any better, please do let me know.