For context, I have a controller method called delete_cars
. Inside of the method, I call destroy_all
on an ActiveRecord::Collection
of Car
s. Below the destroy_all
, I call another method, get_car_nums_not_deleted_from_portal
, which looks like the following:
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
Here, I check to see if any cars were not deleted during the destroy_all
transaction. If there are any, I just add a puts
message. I also return the ActiveRecord::Collection
whether there are any records or not, so the code to follow can handle it.
The goal with one of my feature tests is to mimic a user trying to delete three selected cars, but one fails to be deleted. When this scenario occurs, I display a specific notice on the page stating:
'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}"
How can I force just one record to fail when my code executes the destroy_all
, WITHOUT adding extra code to my Car
model (in the form of a before_destroy
or something similar)? I've tried using a spy, but the issue is, when it's created, it's not a real record in the DB, so my query:
cars_to_be_deleted = Car.where(id: params[:car_ids].split(',').collect { |id| id.to_i })
doesn't include it.
For even more context, here's the test code:
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
Any help with this would be greatly appreciated! It's becoming quite frustrating lol.
CodePudding user response:
One way to "mock" not deleting a record for a test could be to use the block
version of .to receive
to return a falsy
value.
The argument for the block is the instance of the record that would be :destroy
ed.
Since we have this instance, we can check for an arbitrary record to be "not destroyed" and have the block return nil
, which would indicate a "failure" from the :destroy
method.
In this example, we check for the record of the first Car
record in the database and return nil
if it is.
If it is not the first record, we use the :delete
method, as to not cause an infinite loop in the test (the test would keep calling the mock :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
}
You could create a method that accepts a list of records and decide which one you want to :destroy
for more accurate testing!
I am sure there are other ways to work around this, but this is the best we have found so far :)
CodePudding user response:
If there is a specific reason why the deletion might fail you can simulate that case.
Say you have a RaceResult
record that must always refer to a valid Car
and you have a DB constraint enforcing this (in Postgres: ON DELETE RESTRICT
). You could write a test that creates the RaceResult
records for some of your Car
records:
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
Another option would be to use some knowledge of how your controller interacts with the model:
allow(Car).to receive(:destroy_list_of_cars).with(1,2,3).and_return(false) # or whatever your method would return
This would not actually run the destroy_list_of_cars
method, so all the records would still be there in the DB. Then you can expect error messages for each of your selected records.
Or since destroy_all
calls each record's destroy
method, you could mock that method:
allow_any_instance_of('Car').to receive(:destroy).and_return(false) # simulates a callback halting things
allow_any_instance_of
makes tests brittle however.
Finally, you could consider just not anticipating problems before they exist (maybe you don't even need the bulk delete page to be this helpful?). If your users see a more generic error, is there a page they could filter to verify for themselves what might still be there? (there's a lot of factors to consider here, it depends on the importance of the feature to the business and what sort of things could go wrong if the data is inconsistent).