Search code examples
ruby-on-railsrspeccapybararuby-on-rails-6destroy

Is there a way I can force a record to not be destroyed when running a feature test in RSpec? (Rails 6)


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.


Solution

  • 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 :)