RSpec’s stubbing and mocking library (known as rspec-mocks) took me years to understand in practice and use effectively. But after using it regularly throughout my career, it’s among the first places I start in my tests when defining the behavior of the code and then implementing it. In this screencast, I cover creating a complex Order checkout method with tests using RSpec’s mocking and stubbing methods.

The common unit testing pattern with RSpec’s mocking approach is generally to expect methods on an object to be called, something along the lines of:

it "sends an email" do
  expect(Mailer).to receive(:send).with(:new_order, order.email)

  subject.checkout
end

The corresponding code that’s driven out to make that pass is:

class Order
  attr_accessor :email

  def checkout
    Mailer.send(:new_order, @email)
  end
end

The expectation reads pretty naturally. This unit test may seem a bit… silly. We’re expecting code to be called and that’s what happens. We’re not testing what’s returned or any side-effects. Why even bother writing the test in the first place if we could just write the code that does the same thing?

Well, I think there’s value in this type of unit testing because generally we’d start higher up with an end-to-end test or an integration test that gets us to the point of needing Order#checkout to exist. And we use the spec to describe what we want to happen and then use that to design the interfaces (or to call out to already existing interfaces). While the example above is a bit trite, it becomes useful when our method’s needs become more complex.

Let’s say that Order#checkout calls out to a payment gateway that processes the payment that takes an order:

it "processes the payment" do
  expect(PaymentGateway).to receive(:process).with(card)

  subject.checkout
end

The code to make that test pass looks like:

def checkout
  PaymentGateway.process(self)
  Mailer.send(:new_order, @email)
end

This is still a bit simple, and yes, it’s technically unit tested. We’d expect that both PaymentGateway#process and Mailer#send are covered by their own unit tests or we’d add them ourselves to have confidence they work in isolation.

Where this style of mocking gets interesting and exceptionally useful is when we need for branching logic based on return values in the code.

Let’s look at this spec with two contexts to cover the paths:

context "when the payment gateway succeeds" do
  before do
    expect(PaymentGateway).to receive(:process).with(card) { :success }
  end

  it "sends a new order email" do
    expect(Mailer).to receive(:send).with(:new_order, order.email)

    subject.checkout
  end
end

context "when the payment gateway denies the charge" do
  before do
    expect(PaymentGateway).to receive(:process).with(card) { :failure }
  end

  it "does not send a new order email" do
    expect(Mailer).to_not receive(:send).with(:new_order, order.email)

    subject.checkout
  end
end

We now have tests for both paths in the code described… When the payment gateway succeeds, send the email. When it fails, don’t send the email. This matches the needs of our application. Our above #checkout method would cause these specs to fail.

The mocking pattern of using expect(object).to receive(:some_method).with("some args") { "any return val" } is such a useful and common pattern. It makes it clear what’s expected to happen right away in the test. The block at the end is what’s returned by the method call. There are a lot of options for #with and other calls that can be chained to this to have more specific expectations, but they’re beyond the scope of this post.

To make these tests path, we’d end up with:

def checkout
  if PaymentGateway.process(self) == :success
    Mailer.send(:new_order, @email)
  end
end

While our #checkout method is quite simple still, it’s likely processing an order has more needs than just that. When methods aren’t as simple as passing in a value and expecting something to be returned, this approach of mocking in the tests makes it much easier to cover the behavior and have confidence in the isolated unit-level behavior of the object and method under test.

Additional Resources