Reputation: 63
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.
Upvotes: 0
Views: 118
Reputation: 106862
There are at least three problems in this test.
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
First, you add a spy to benefit
in the first line, but then you set benefit
to another instance in line three. And the instance returned by @contract.benefit
is not the same instance that benefit
defined in line one or line three. It is the same record in the database than defined in line three, but it is not the exact same instance in memory. Same for @contract
, because you are setting the contract_id
in the factory bot call, the callback reloads the contract by its id
from the database and touches the newly loaded record and not the one you added the spy to.
Unfortunately, you didn't share how you set @contract
, but I guess the following might work for you:
let(:contract) { @contract }
before { allow(contract).to receive(:touch).and_call_original }
it 'should touch contract on benefit creation when company is active' do
create(:benefit, benefit_type: :ahc, contract: contract)
expect(contract).to have_received(:touch).once
end
Another option might be to not add a spy to the touch
method but instead test that the contract.updated_at
column actually changes when creating a suitable benefit
. Because the creating and the touch happens almost at the same time, it might make sense to travel in time to make the change more obvious. For example, like this:
let(:contract) { @contract }
let(:time) { 10.minutes.from_now }
it 'should touch contract on benefit creation when company is active' do
travel_to(time) do
expect {
create(:benefit, benefit_type: :ahc, contract: contract)
}.to change { contract.updated_at }.to(time)
end
end
Upvotes: 0
Reputation: 28305
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.
Upvotes: 1