Home > Software engineering >  Is there a way I can force a record to not be destroyed when running a feature test in RSpec? (Rails
Is there a way I can force a record to not be destroyed when running a feature test in RSpec? (Rails

Time:02-01

For context, I have a controller method called delete_cars. Inside of the method, I call destroy_all on an ActiveRecord::Collection of Cars. 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 :destroyed.

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).

  • Related