Search code examples
ruby-on-railsactiverecordintegration-testingrspec-rails

RSpec not seeing changes to ActiveRecord object with `change(object, :message)` syntax


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.


Solution

  • 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.