Я работаю над написанием тестового примера для модели 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
Они будут отличаться в микросекундах.
В этом случае @contract.reload должен ответить, что получено. Это также терпит неудачу.
Как загрузить или инициализировать @contact
и benefit
? Вы уверены, что benefit
— это тот же экземпляр Benefit
, что и тот, который вызывается из модели? Тот же экземпляр, а не одна и та же запись из БД?
(a)контракт создается с использованием заводского бота. Когда я создаю объект преимущества, я могу остановить привязку.pry непосредственно перед контрактом.touch в модели. Но до сих пор не могу понять, почему он не отвечает. Я также подумал использовать выгоду.contract для have_received и @contract на случай, если будет выбран какой-то другой контракт.
Вы добавляете льготу за застенчивость в первой строке, но затем отменяете выгоду в третьей строке.
Пожалуйста, дополните. Или напишите решение этой проблемы.
Может быть, попробовать добавить allow(benefit).to receive(:contract).and_return(@contract)
? Также комментарий @spickerman был связан с тем фактом, что переменная benefit
, указанная в строке 1, перезаписывается в строке 3, поэтому строка 1 benefit
не является тем же объектом, что и строка 4 benefit
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
.
Сначала позвольте мне ответить на вопрос, который вы на самом деле задали. Давайте проверим, что 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
Я не фанат вашего нынешнего подхода к тестированию, потому что он тестирует реализацию, а не поведение.
Некоторые люди могут возразить, что ваш нынешний подход на самом деле лучше, поскольку он может работать, не затрагивая базу данных, но вот альтернативный способ:
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
. Меня волнует только то, как изменилась временная метка.
Это помогло. Я изменил ваше решение, чтобы лучше понять, как это работает.
В этом тесте есть как минимум три задачи.
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
Ваше первое решение не сработало. Второй вариант чем-то похож на мой. Но все равно спасибо, узнал другой подход к тому же.
К сожалению, «не сработало» — не совсем точно. Не могли бы вы поделиться дополнительной информацией о том, что не сработало и какое сообщение об ошибке вы получили? И, возможно, как вы на самом деле справились с этой проблемой? Я хотел бы улучшить свой ответ, чтобы помочь другим читателям в будущем, которые столкнулись с аналогичной проблемой и наткнулись на эту страницу.
create(:benefit, benefit_type: :ahc, contract_id: @contract.id)
– при передаче идентификатора контракта модель получит свой экземпляр из базы данных и обновит его вместо вашего.