Bharat Kumar Anand
Bharat Kumar Anand

Reputation: 63

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.

Upvotes: 0

Views: 118

Answers (2)

spickermann
spickermann

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

Tom Lord
Tom Lord

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.


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.

Upvotes: 1

Related Questions