rubyruby-on-rails-4rspecrspec-rails

Rspec not able to trigger method inside after_commit callback


I am working on writing a test case for a model Benefit. The class file contains an after_commit callback which calls a method update_contract. It also has belongs_to :contract, touch: true.

@contract is created in the before action of the spec.

def update_contract
    return unless {some condition}
    contract.touch
end
it 'should touch contract on benefit creation when company is active' do
    allow(benefit).to receive(:update_contract)
    allow(@contract).to receive(:touch)
    benefit = create(:benefit, benefit_type: :ahc, contract_id: @contract.id)
    expect(benefit).to have_received(:update_contract)
    expect(@contract).to have_received(:touch)
end

When I manually added touch logic just above expect, it responded to the have_received.

I have tried

benefit.run_callbacks(:commit), use_transactional_fixtures is false in the system. 

benefit receives the update_contract method that is working correctly. But the @contract is not to the have received.

This is working though

@contract was created at runtime of spec, and benefit created little after

original_updated_at = @contract.updated_at
:created_benefit
@contract.updated_at != original_updated_at

They will differ in microseconds.


Solution

  • Rspec not able to trigger method inside after_commit callback

    Yes it is. The trigger is being called.

    However, your test expectations won't work. First you set it up like this:

    allow(benefit).to receive(:update_contract) # 1
    allow(@contract).to receive(:touch) # 2
    benefit = create(:benefit, benefit_type: :ahc, contract_id: @contract.id) # 3
    

    This won't pass:

    expect(benefit).to have_received(:update_contract)
    

    because the spy you set on on line 1 is a different object to line 3.

    And this won't pass:

    expect(@contract).to have_received(:touch)
    

    because the spy you set on line 2 is a different object to what's fetched by the model in Benefit#update_contract.


    How to fix it - option 1

    First let me answer the question you actually asked. Let's verify that touch is being called

    before do
      @contract = create(:contract ....)
    end
    
    it 'should touch contract on benefit creation when company is active' do
        # Don't save it to the database yet, so no callbacks are triggered.
        benefit = build(:benefit, benefit_type: :ahc, contract_id: @contract.id)
    
        allow(benefit).to receive(:update_contract)
        # Make sure we return the same object!
        allow(benefit).to receive(:contract).and_return(contract)
        allow(@contract).to receive(:touch)
    
        # Or you could call `save` here. Both should work.
        benefit.run_callbacks(:commit)
    
        expect(benefit).to have_received(:update_contract)
        expect(@contract).to have_received(:touch)
    end
    

    How to fix it - option 2

    I'm not a fan of your current testing approach, because it's testing implementation, instead of behaviour.

    Some people might argue that your current approach is actually better since it can run without touching the database, but here's an alternative way:

    before do
      @contract = create(:contract ....)
    end
    
    it 'should touch contract on benefit creation when company is active' do
        original_updated_at = @contract.updated_at
        create(:benefit, benefit_type: :ahc, contract_id: @contract.id)
    
        expect(@contract.reload.updated_at).not_to eq(original_updated_at)
    end
    

    There are many variations on how exactly to write that, e.g. you could use freeze_time and check the exact timestamp. Or you could format the test a little differently, calling expect with a block, and giving from and to as expectations.

    But however you go about it, the fundamental difference is: I don't know/care what the implementation is with after_commit callbacks. All I care about is the behaviour that the timestamp has changed.