Test Spies


Test spies are a wonderful tool to utilize in the RSpec testing environment. When used in moderation and with care. Test spies require that a called method be stubbed so that it can be checked to see how it was invoked.

I have put together a really simple class to demonstrate a test spy.

class NotifyUser
  attr_reader :user

  def initialize(user)
    @user = user
  end

  def execute(params={})
    mailer.notify({ message: params[:message] })
  end

  def mailer
    SomeMailer.new(user)
  end
end

Notice in the test below that a test spy follows a pattern. There is a mock up section, an excercise section, and a verification section. Each of these are important and I always put a space in between the sections. It’s much easier to see what is happening in the test.

describe NotifyUser do
  let(:user) { double('User') }
  let(:command) { NotifyUser.new(user) }

  describe '#execute' do
    it 'sends the notification to the user' do
      # Mock up
      mailer = double('SomeMailer', notify: true)
      command.stub(mailer: mailer)

      # Excercise
      command.execute({message: 'Hello'})

      # Verification
      expect(mailer).to have_received(:notify).with({ message: 'Hello'})
    end
  end
end

Mocking up outside of the it block should be kept to a minimum. This is because it can get to be a little hectic trying to understand what the test is doing. I like tests that are readable and succinct.

If the length of a test file is forcing me to start mocking outside of it blocks, I like take a step back and ask myself, “Is this class really complex?” It is important to realize when tests are getting more and more difficult to maintain, that the code base is most likely really coupled.

When to use them

Test spies are not meant to be used everywhere. I typically use them when a class is communicating with an external object. I will stub the method that wraps the object and make it return a double. This is so if the code base does change, this test will fail quickly and force the developer to look at what it failed and possibly refactor tests.

Do not apply liberally, but do apply where necessary.