By

Mocking with Interdependency in RSpec

I’ve used RSpec for a while for structuring and testing code, and although it has its critics, I find the flexibility of its DSL to be really useful.

The other day I encountered a situation in a controller test that I hadn’t dealt with before, where that flexibility came in pretty handy. Here’s the action I was testing:

def create
  if current_order.process_payment! && current_order.next!
    cookies.delete(:guest_token)
    redirect_to current_order_path
  else
    render text: rendered_view
  end
end

This is the confirmation step in a cart flow, where a payment is processed and, if successful, the user is redirected to the complete page. The code was working fine but the test was failing.

Here’s the original test code:

describe "POST create" do
  before do
    expect(order).to receive(:process_payment!).once.and_return(paid?)
  end

  context "successful capture" do
    let(:paid?) { true }
    before do
      allow(order).to receive(:paid?).and_return(true)
      post :create
    end
    
    it "redirects to cart complete page" do
      expect(response).to redirect_to cart_complete_url }
    end
  end
...
end

So we’re setting an expectation on process_payment!, which we want to check is called in all cases without exception. We are returning true here because we want to see what happens when payment is successful. When it is successful, order.next! is called, advancing the cart to the next step. If that returns true we redirect to the order complete page (if not then render the confirm page again with an error).

The problem here is that the order state transition logic depends on the payment state: an order that is paid cannot go (back) to the “confirm” state. A before_action in each cart controller step checks the value of paid? and redirects accordingly.

So you’re in a Catch-22, because paid? is being called twice, and should return a different value in each case. If you stub paid? to return true, but do not actually change the payment state, current_order.next! fails. If you set it to return true, then the order has already been paid and the controller jumps to the complete page without ever processing the payment, so the expectation fails (as it should).

What we want is for the mock to return different values the first and second time it is called. RSpec can actually do this with consecutive return values, so e.g. you could this:

allow(order).to receive(:paid?).and_return(false, true)

and the test would pass. The thing is, it would pass regardless of whether process_payment! had been called before or after the second paid? was called, so you could get a passing test when in reality the controller action would fail to work correctly.

What we really want here is for paid? to return true if and only if process_payment! has been called. Surprisingly (or maybe not), that’s trivially easy to do by using RSpec’s block implementation to set an instance variable:

before do
  @paid = false
  expect(order).to receive(:process_payment!).once do
    @paid = true
  end
  allow(order).to receive(:paid?) { @paid }
  post :create
end

And now, the test passes and actually makes sense: we are stubbing out the payment logic behind paid? and replacing it by a minimal implementation which returns true once process_payment! has been called on the order. This is really just a special use case of RSpec’s block implementation, but I found it surprisingly simple and easy to use.

Looking for a Rails developer
Degica is looking for a Rails developer to work with our dev team. Checkout the link for more information.