Rspec не может запустить метод внутри обратного вызова after_commit

Я работаю над написанием тестового примера для модели Benefit. Файл класса содержит обратный вызов after_commit, который вызывает метод update_contract. Он также имеет own_to :contract, touch: true.

@contract создается в предыдущем действии спецификации.

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

Когда я вручную добавил сенсорную логику чуть выше ожидаемой, она ответила на has_received.

я пытался

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

выгода получает метод update_contract, который работает правильно. Но @contract не получен.

Хотя это работает

@contract был создан во время выполнения спецификации, а выгода появилась вскоре после этого.

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

Они будут отличаться в микросекундах.

create(:benefit, benefit_type: :ahc, contract_id: @contract.id) – при передаче идентификатора контракта модель получит свой экземпляр из базы данных и обновит его вместо вашего.
Stefan 21.05.2024 07:47

В этом случае @contract.reload должен ответить, что получено. Это также терпит неудачу.

Bharat Kumar Anand 21.05.2024 08:03

Как загрузить или инициализировать @contact и benefit? Вы уверены, что benefit — это тот же экземпляр Benefit, что и тот, который вызывается из модели? Тот же экземпляр, а не одна и та же запись из БД?

spickermann 21.05.2024 11:58

(a)контракт создается с использованием заводского бота. Когда я создаю объект преимущества, я могу остановить привязку.pry непосредственно перед контрактом.touch в модели. Но до сих пор не могу понять, почему он не отвечает. Я также подумал использовать выгоду.contract для have_received и @contract на случай, если будет выбран какой-то другой контракт.

Bharat Kumar Anand 21.05.2024 12:42

Вы добавляете льготу за застенчивость в первой строке, но затем отменяете выгоду в третьей строке.

spickermann 21.05.2024 14:37

Пожалуйста, дополните. Или напишите решение этой проблемы.

Bharat Kumar Anand 21.05.2024 19:15

Может быть, попробовать добавить allow(benefit).to receive(:contract).and_return(@contract)? Также комментарий @spickerman был связан с тем фактом, что переменная benefit, указанная в строке 1, перезаписывается в строке 3, поэтому строка 1 benefit не является тем же объектом, что и строка 4 benefit

engineersmnky 22.05.2024 17:42
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Шаг 1: Создание приложения Slack Чтобы создать Slackbot, вам необходимо создать приложение Slack. Войдите в свою учетную запись Slack и перейдите на...
0
7
58
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Rspec не может запустить метод внутри обратного вызова after_commit

Да, это. Триггер вызывается.

Однако ваши тестовые ожидания не сработают. Сначала вы настраиваете его так:

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

Это не пройдет:

expect(benefit).to have_received(:update_contract)

потому что шпион, которого вы установили в строке 1, отличается от объекта в строке 3.

И это не пройдет:

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

потому что шпион, которого вы установили в строке 2, — это объект, отличный от того, что получено моделью в Benefit#update_contract.


Как исправить - вариант 1

Сначала позвольте мне ответить на вопрос, который вы на самом деле задали. Давайте проверим, что touch вызывается

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

Как исправить - вариант 2

Я не фанат вашего нынешнего подхода к тестированию, потому что он тестирует реализацию, а не поведение.

Некоторые люди могут возразить, что ваш нынешний подход на самом деле лучше, поскольку он может работать, не затрагивая базу данных, но вот альтернативный способ:

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

Существует много вариантов того, как именно это написать, например. вы можете использовать freeze_time и проверить точную отметку времени. Или вы могли бы отформатировать тест немного по-другому, вызывая ожидание с блоком и указывая от и до как ожидания.

Но как бы вы это ни делали, фундаментальная разница заключается в следующем: мне все равно, какова реализация обратных вызовов after_commit. Меня волнует только то, как изменилась временная метка.

Это помогло. Я изменил ваше решение, чтобы лучше понять, как это работает.

Bharat Kumar Anand 29.05.2024 10:45

В этом тесте есть как минимум три задачи.

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

Сначала вы добавляете шпиона в benefit в первой строке, но затем устанавливаете benefit в другой экземпляр в третьей строке. И экземпляр, возвращаемый @contract.benefit, не тот экземпляр, который benefit определен в первой или третьей строке. Это та же запись в базе данных, что и в третьей строке, но это не тот же самый экземпляр в памяти. То же самое и с @contract, поскольку вы устанавливаете contract_id в заводском вызове бота, обратный вызов перезагружает контракт по его id из базы данных и касается только что загруженной записи, а не той, к которой вы добавили шпиона.

К сожалению, вы не рассказали, как вы ставили @contract, но думаю, вам подойдет следующее:

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

Другой вариант — не добавлять шпиона в метод touch, а вместо этого проверить, действительно ли столбец contract.updated_at меняется при создании подходящего benefit. Поскольку создание и прикосновение происходят почти одновременно, возможно, имеет смысл путешествовать во времени, чтобы сделать изменения более очевидными. Например, вот так:

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

Ваше первое решение не сработало. Второй вариант чем-то похож на мой. Но все равно спасибо, узнал другой подход к тому же.

Bharat Kumar Anand 29.05.2024 10:47

К сожалению, «не сработало» — не совсем точно. Не могли бы вы поделиться дополнительной информацией о том, что не сработало и какое сообщение об ошибке вы получили? И, возможно, как вы на самом деле справились с этой проблемой? Я хотел бы улучшить свой ответ, чтобы помочь другим читателям в будущем, которые столкнулись с аналогичной проблемой и наткнулись на эту страницу.

spickermann 29.05.2024 13:00

Другие вопросы по теме