Using Rspec should_receive to test that a controller calls a method on an object correctly

521 views Asked by At

OK first, I should say while I've read a lot about should_receive, I'm still not entirely sure I'm understanding the concept behind it, so what I'm doing could potentially be completely not possible.

I have the following:

class PlansController
  def destroy
    plan = plan.find_by_id(params[:id])
    if plan.cancel_stripe_subscription(params[:reason])
      flash[:success] = "success"
      redirect_to root_path
    else
      #error handling
    end
  end
end

class Plan
  def cancel_stripe_subscription(reason)
    self.status = "canceled"
    self.cancellation_reason = reason
    if self.save
      return true
    else
      return false
    end
  end

In my controller spec, I am thinking it makes sense to do a test that the cancel_stripe_subscription method is called successfully (using 1should_receive1), with the right arguments and everything, and another test that the output of the destroy action is correct.

In other words, I thought to write the following controller spec:

describe PlansController, "Destroy Action" do
  before do
    @plan = Plan.create(...)
  end
  it "should have called destroy action" do
    delete :destroy,
      plan: {
        id: @plan.id,
        reason: "something"
      }
      assigns(:plan).should_receive(:cancel_stripe_subscription).with(reason:"something").exactly(1).times.and_return(true)
  end

  it "should have called destroy action" do
    delete :destroy,
      plan: {
        id: @plan.id,
        reason: "something"
      }
    assigns(:plan).status.should == "canceled"
    assigns(:plan).cancellation_reason.should == "something"
  end
end

The second test passes, but the first throws

Failure/Error: assigns(:plan).should_receive(:cancel_stripe_subscription)
   (#<Plan:0x007fe282931310>).cancel_stripe_subscription(*(any args))
       expected: 1 time with any arguments
       received: 0 times with any arguments

So I really have two questions:

  1. Just to confirm, am I using should_receive correctly? Should I even be testing for this? Or is the second test generally accepted as enough?
  2. If I should be testing for this, what's the right way of using should_receive? (Note, have not had luck with expect(@plan).to have_received(:cancel_stripe_subscription) either)
2

There are 2 answers

3
Wally Altman On BEST ANSWER

A should_receive expectation has to be set before you call the method under test; you're setting it afterwards. Since you need to set it before, you then have to make sure the object you've set up the expectation on ends up being operated on in the action. The normal way would be to stub out find_by_id on Plan, like this:

it "should have called destroy action" do
  Plan.stub(:find_by_id).and_return(@plan)
  assigns(:plan).should_receive(:cancel_stripe_subscription).with(reason:"something").exactly(1).times.and_return(true)
  delete :destroy, plan: { id: @plan.id, reason: "something" }
end

(I am assuming that you meant to write plan = Plan.find_by_id(params[:id]) in the first line of your destroy action.)

As to whether you should be testing it this way, I'd say that your second test does a good enough job of verifying the outcome that you want, and you don't really need to go to all the trouble.

0
Andy Waite On

I think the confusion here is in part due to combining two different styles of testing, mockist (your first test) and classicist (your second test). It's fine to use one or the other, based on your preferred testing style, but using both together to test the same piece of code is somewhat redundant.