The letter A styled as Alchemists logo. lchemists
Published March 1, 2022 Updated March 10, 2022
Cover
RSpec Test Doubles

I’m a huge fan of RSpec because it’s a testing framework and a specification for describing application behavior in Ruby. Test coverage — along with behavioral understanding — is a lot of value provided by a single framework (not to mention having an excellent Domain Specific Language (DSL) for writing expressive specs as well).

For today, I want to focus on test doubles only. Namely:

  • Stubs

  • Mocks

  • Spies

  • Fakes

I’ve, unfortunately, worked with a lot of test suites which confuse how test doubles are meant to be used. So I want to spend time talking about each, what they are, and why they are important (or not 😅).

Setup

Should you wish to follow along within your local development environment, feel free to use Rubysmith to automatically generate a working setup for you. Here’s how to get started (totally optional, reading along is fine too):

gem install rubysmith
rubysmith --build demo

Once generated, open demo.rb in your editor and add the following:

require "logger"

class Demo
  def initialize logger: Logger.new(STDOUT)
    @logger = logger
  end

  def call(message) = logger.info message

  private

  attr_reader :logger
end

Here we are injecting a logger dependency so we can test our Demo implementation logs messages as desired. The Demo implementation isn’t meant to be tremendously interesting — which is fine — but how we go about testing the implementation will be.

To complete your setup, create a demo_spec.rb for experimentation purposes. Example:

require "spec_helper"

RSpec.describe Demo do
  subject(:demo) { described_class.new logger: }

  let(:logger) { Logger.new $stdout }

  describe "#call" do
    it "logs information" do
      expectation = proc { demo.call "test" }
      expect(&expectation).to output(/test/).to_stdout
    end
  end
end

All we’re doing is ensuring our Demo implementation can log a specific message. I’m using a closure (i.e. proc) to wrap our implementation in a block so our expectation can be validated in a single line. Otherwise, using block syntax when composing your expect statement will span multiple lines and gets unseemly, fast. Now everything succinctly fits on a single line for improved readability.

By the way, if you run the above, you should see specs are green (assuming you still are following along locally, if you like).

Configuration

When it comes to configuring test doubles within RSpec, there are two important configuration settings you need:

RSpec.configure do |config|
  # Truncated for brevity.

  config.expect_with :rspec do |expectations|
    expectations.syntax = :expect
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_doubled_constant_names = true
    mocks.verify_partial_doubles = true
  end
end

The first block ensures you are using expect syntax for all specs while the chain setting ensures failure message include clauses from methods defined using chain. Hopefully, you don’t have to chain messages but, if you do, at least you’ll have more diagnostic information.

The second block ensures mocked constants and partial doubles are verified. This is the most important of the two blocks because verification ensures that — if your implementation changes but your specs aren’t updated — then your specs will fail. You definitely want this.

Test Doubles

Now that setup and configuration are out of the way, let’s talk about test doubles by starting with stubs.

Stubs

Stubs are a quick way to answer canned responses to sent messages. In our case, all we need to do is stub the #info message sent to the injected logger to verify our implementation. This means we can rewrite our original spec as follows:

it "logs information" do
  allow(logger).to receive(:info).with("test") { puts "test" }
  expectation = proc { demo.call "test" }

  expect(&expectation).to output(/test/).to_stdout
end

In this case I’ve allowed the logger to receive an #info message with the string "test" and print to the console. Normally, you’d allow your stub to return a message using #and_return. Example:

allow(logger).to receive(:info).with("test").and_return("test")

…​but since we are testing that messages are logged to the console, we can use a block to mimic expected behavior instead.

Stubs are also known as partial doubles because they are combination of original implementation plus mocked behavior. They are also considered a code smell because they are a superficial way to test your implementation while masking over a deeper implementation flaw which probably needs to be refactored. This is why I seldom use stubs. So let’s talk about mocks next to improve upon this further.

Mocks

Mocks allow you to test for specific behavior or throw an error otherwise. They come in three flavors:

  • double: A non-verifying mock and not recommended because if your implementation changes your spec will still pass.

  • class_double: A verifying mock for classes only.

  • instance_double: A verifying mock for instances only.

All of the above works the same, syntactically, but since we’ve injected an instance of our logger into our implementation, I’ll only demo the use of an instance_double. Here’s how the spec changes when using mocks:

RSpec.describe Demo do
  subject(:demo) { described_class.new logger: }

  let(:logger) { instance_double Logger }

  describe "#call" do
    it "logs information" do
      expect(logger).to receive(:info).with("test")
      demo.call "test"
    end
  end
end

Notice how I’ve changed the logger to be an instance double (mock) of the Logger class in the let statement and then, in the spec, I expect the logger to receive a message before messaging our implementation. There are couple important observations I want to highlight with this approach:

  • This spec uses fewer lines of code than our earlier, stubbed, spec which is definitely nice.

  • This spec, unfortunately, reads backwards because we must set up our expectation before we message our implementation.

Due to fact that our spec reads backwards, this is why I avoid using mocks because reading specs backwards — starting from the bottom of the spec and reading upwards — is not normal and also jarring when quickly attempting to grok what is going on.

This brings us to my personal favorite: spies.

Spies

Like mocks, spies record received messages and error if not recorded. They come in three flavors as well:

  • spy: A non-verifying spy and not recommended because if your implementation changes your spec will still pass.

  • class_spy: A verifying spy for classes only.

  • instance_spy: A verifying spy for instances only.

Since we’re only dealing with an instance of a logger class in our implementation I’ll use the instance_spy for demonstration purposes but the syntax is the same for all three.

Here’s how we update the spec to use a spy:

RSpec.describe Demo do
  subject(:demo) { described_class.new logger: }

  let(:logger) { instance_spy Logger }

  describe "#call" do
    it "logs information" do
      demo.call "test"
      expect(logger).to have_received(:info).with("test")
    end
  end
end

Notice how elegant the above is and uses less code than the mock example. The differences might are subtle but powerful:

  • The instance_double is replaced with instance_spy to build a spy of the Logger class.

  • The spec reads sequentially instead of backwards (unlike with the earlier mock). First we message our implementation and then we check the logger got the intended message. Very readable and natural.

  • The have_received message is used instead of received. This is an important distinction when using spies because you are checking that the message was received after the fact. Plus, it reads logically in terms of past tense which is nice.

Due to the above reasons, spies are far more superior than mocks in this regards and definitely encourage using them over mocks.

There is one last test double to talk about and that’s fakes.

Fakes

Technically, fakes, are not specific to RSpec but I want to talk about them because they are often overlooked when talking about test doubles and good to have in your arsenal of testing tools. They do require a bit more setup and maintenance, though.

Fakes are a way to swap out an existing implementation with an identical Object API which is great for speeding up your test suite when your original implementation might be CPU or I/O intensive. For example, making API requests to third party services or any process that is slow to respond, occasionally times out, etc.

In our case, our logger isn’t CPU or I/O intensive but we can still make a fake of it. Here’s what a very naive fake logger would look like:

require "spec_helper"

RSpec.describe Demo do
  subject(:demo) { described_class.new logger: logger.new(STDOUT) }

  let :logger do
    Class.new do
      def initialize io
        @io = io
      end

      def debug(...) = io.puts(...)

      def info(...) = io.puts(...)

      def warn(...) = io.puts(...)

      def error(...) = io.puts(...)

      def fatal(...) = io.puts(...)

      def unknown(...) = io.puts(...)

      private

      attr_reader :io
    end
  end

  describe "#call" do
    it "logs information" do
      expectation = proc { demo.call "test" }
      expect(&expectation).to output(/test/).to_stdout
    end
  end
end

⚠️ For simplicity, I’ve inlined the fake logger within an anonymous class via the let statement. I’ve also made the implementation be a simple pass though to any I/O object but avoided handling block syntax. Fleshing out more of those details is out of scope for this article. The point is only to showcase the pattern of a fake object. Lastly, for improved reusability, I’d recommend swapping out the let statement for a properly named FakeLogger class which you can reuse in multiple specs.

That aside, you’ll notice that our spec is back to testing our injected logger dependency — as first shown at the start of this article. At this point, you could swap out the fake logger with the real logger and see no change in spec behavior. That is the beauty of using a fake but, as mentioned earlier, it does come with a maintenance cost so only use fakes when you need them for CPU or I/O intense situations.

Conclusion

I hope you’ve enjoyed this brief tour of stubs, mocks, spies, and fakes — also known as test doubles — in RSpec. I also hope this demystifies any confusion you might have when it comes to reaching for any one of these test doubles too.

Finally, always reach for a spy first and a fake second. Now you can ignore stubs and mocks for a much easier to read and maintain test suite. May your test suite be a joy to work with. 🎉