davegson
davegson

Reputation: 8331

ActiveRecord: Mock has_many relation call

I'm fairly new to testing, so I've been struggling with using the correct syntax, especially regarding mocks.

I want to test my destroy action in cars_controller.rb

def destroy
  if current_user.cars.exists?(params[:id])
    car = current_user.cars.find(params[:id])

    # only destroy the car if it has no bookings
    car.destroy unless car.bookings.exists?
  end

  redirect_to user_cars_path(current_user)
end

It was rather easy testing the case, when no bookings are associated with the car.

describe CarsController, type: :controller do

  let(:user) { create(:user_with_car) }
  before { login_user(user) }

  describe "DELETE #destroy" do
    let(:car) { user.cars.first }

    context "when the car has no bookings associated to it" do
      it "destroys the requested car" do
        expect {
          delete :destroy, user_id: user.id, id: car.id
        }.to change(user.cars, :count).by(-1)
      end
    end

But this test is driving me nuts:

    context "when the car has bookings associated to it" do
      it "does not destroy the requested car" do

        ##### This line fails miserably
        allow(car).to receive_message_chain(:bookings) { [ Booking.new ]}

        expect {
          delete :destroy, user_id: user.id, id: car.id
        }.to change(user.cars, :count).by(0)
      end
    end
  end
end

I do not want to create bookings in the database and associate them with the car. As I understand, it is recommended to mock these bookings, since they have no use further on.

Next to:

allow(car).to receive_message_chain(:bookings) { [ Booking.new ]}

I've had multiple attempts with other syntax', but all failed. I even tried using rpsec-mocks old syntax: stub(...).

How would I accomplish this?

Upvotes: 0

Views: 1139

Answers (1)

Taryn East
Taryn East

Reputation: 27747

The reason this isn't working, is that the delete action loads its own version of car - it isn't using the local variable you have declared locally to your spec. So any stubs you add to your local variable will not actually exist on the brand new copy of car that is inside the controller action.

There are a couple of ways around this.

  1. One is to stub out the car that your controller fetches.
  2. Another is to stub out any_instance_of(Car)
  3. The third is to just actually set up a booking for the car you have...

The difference between these options is a tradeoff between being tightly entangled with the internals of your code (ie harder to maintain), speed of running, or actually testing out all the aspects of your code.

The third one makes sure that everything really works (you have a real car with a real booking), but it's slower because it sets up actual models in the db... and that's what you're stubbing to get past.

The first and second are up to you. I personally have an "ick" feeling about stubbing out find when you're testing a controller where finding the car is part of what hopefully you're testing...

plus - it's only ever going to find the car you set up before, so you might as well do a stub on any instance.

SO:

expect_any_instance_of(Car).to receive(:bookings).and_return([ Booking.new ])`

will probably do the trick.

Rspec any_instance_of doco

Upvotes: 1

Related Questions