Для контекста у меня есть метод контроллера под названием delete_cars
. Внутри метода я вызываю destroy_all
для ActiveRecord::Collection
из Car
s. Под destroy_all
я вызываю другой метод, get_car_nums_not_deleted_from_portal
, который выглядит следующим образом:
def get_car_nums_not_deleted_from_portal(cars_to_be_deleted)
reloaded_cars = cars_to_be_deleted.reload
car_nums = reloaded_cars.car_numbers
if reloaded_cars.any?
puts "Something went wrong. The following cars were not deleted from the portal: #{car_nums.join(', ')}"
end
car_nums
end
Здесь я проверяю, не были ли удалены какие-либо автомобили во время транзакции destroy_all
. Если они есть, я просто добавляю сообщение puts
. Я также возвращаю ActiveRecord::Collection
независимо от того, есть какие-либо записи или нет, поэтому следующий код может это обработать.
Целью одного из моих функциональных тестов является имитация пользователя, пытающегося удалить три выбранных автомобиля, но один из них не удаляется. Когда происходит этот сценарий, я отображаю на странице специальное уведомление, в котором говорится:
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{some_car_numbers_go_here}"
Как я могу заставить только одну запись завершиться ошибкой, когда мой код выполняет destroy_all
, БЕЗ добавления дополнительного кода в мою Car
модель (в виде before_destroy
или чего-то подобного)? Я пытался использовать шпиона, но проблема в том, что когда он создается, это не настоящая запись в БД, поэтому мой запрос:
cars_to_be_deleted = Car.where(id: params[:car_ids].split(',').collect { |id| id.to_i })
не включает его.
Для еще большего контекста, вот тестовый код:
context 'when at least one car is not deleted, but the rest are' do
it "should display a message stating 'Some selected cars have been successfully...' and list out the cars that were not deleted" do
expect(Car.count).to eq(100)
visit bulk_edit_cars_path
select(@location.name.upcase, from: 'Location')
select(@track.name.upcase, from: 'Track')
click_button("Search".upcase)
find_field("cars_to_edit[#{Car.first.id}]").click
find_field("cars_to_edit[#{Car.second.id}]").click
find_field("cars_to_edit[#{Car.third.id}]").click
click_button('Delete cars')
cars_to_be_deleted = Car.where(id: Car.first(3).map(&:id)).ids
click_button('Yes')
expect(page).to have_text(
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{@first_three_cars_car_numbers[0]}".upcase
)
expect(Car.count).to eq(98)
expect(Car.where(id: cars_to_be_deleted).length).to eq(1)
end
end
Любая помощь в этом будет принята с благодарностью! Это становится довольно разочаровывающим, лол.
🤔 А знаете ли вы, что...
Одной из ключевых особенностей Rails является активная запись (Active Record) - ORM-система, которая упрощает взаимодействие с базой данных.
Если есть конкретная причина, по которой удаление может завершиться неудачей, вы можете смоделировать этот случай.
Допустим, у вас есть запись RaceResult
, которая всегда должна ссылаться на допустимую Car
, и у вас есть ограничение БД, обеспечивающее это (в Postgres: ON DELETE RESTRICT
). Вы можете написать тест, который создает записи RaceResult
для некоторых из ваших записей Car
:
it 'Cars prevented from deletion are reported` do
...
do_not_delete_cars = Car.where(id: Car.first(3).map(&:id)).ids
do_not_delete_cars.each { |car| RaceResult.create(car: car, ...) }
click_button('Yes')
expect(page).to have_text(...
end
Другой вариант — использовать некоторые знания о том, как ваш контроллер взаимодействует с моделью:
allow(Car).to receive(:destroy_list_of_cars).with(1,2,3).and_return(false) # or whatever your method would return
На самом деле это не запустит метод destroy_list_of_cars
, поэтому все записи останутся в БД. Затем вы можете ожидать сообщения об ошибках для каждой из выбранных вами записей.
Или, поскольку destroy_all
вызывает метод destroy
каждой записи, вы можете имитировать этот метод:
allow_any_instance_of('Car').to receive(:destroy).and_return(false) # simulates a callback halting things
allow_any_instance_of
однако делает тесты хрупкими.
Наконец, вы могли бы подумать о том, чтобы просто не предвидеть проблемы до того, как они возникнут (может быть, вам даже не нужна страница массового удаления, чтобы быть такой полезной?). Если ваши пользователи видят более общую ошибку, есть ли страница, которую они могли бы отфильтровать, чтобы проверить, что там еще может быть? (Здесь нужно учитывать множество факторов, это зависит от важности функции для бизнеса и того, какие вещи могут пойти не так, если данные несовместимы).
Одним из способов «сымитировать» удаление записи для теста может быть использование block
версии .to receive
для возврата значения falsy
.
Аргументом для блока является экземпляр записи, которая будет :destroy
ed.
Поскольку у нас есть этот экземпляр, мы можем проверить произвольную запись на «не уничтоженность» и вернуть блок nil
, что укажет на «сбой» из метода :destroy
.
В этом примере мы проверяем запись первой Car
записи в базе данных и возвращаем nil
, если она есть.
Если это не первая запись, мы используем метод :delete
, чтобы не вызвать бесконечный цикл в тесте (тест будет продолжать вызывать макет :destroy
).
allow_any_instance_of(Car).to receive(:destroy) { |car|
# use car.delete to prevent infinite loop with the mocked :destroy method
if car.id != Car.first.id
car.delete
end
# this will return `nil`, which means failure from the :destroy method
}
Вы можете создать метод, который принимает список записей и решать, какую из них вы хотите :destroy
для более точного тестирования!
Я уверен, что есть другие способы обойти это, но это лучшее, что мы нашли до сих пор :)