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