Search code examples
ruby-on-railsrubyrspec

What's the best way to organize and/or store a mock object I consistently use in RSpec?


I built a client class that sends requests to the Discord API. I mock this client as showed in the sample code below. Please see method #mock_client:

require 'rails_helper'
require 'discord_client'

RSpec.describe some_service_class do
  describe '#call' do
    let(:client) { mock_client }

    it 'does this using discord_client' do
      client
      
      described_class.new.call
      
      expect(client).to have_received(:new).once
      expect(client).to have_received(:get_guild).once
    end
  end

  private

  def mock_client
    client = instance_double(DiscordClient)

    allow(DiscordClient).to receive(:new).and_return(client)
    allow(client).to receive(:get_guild)
    allow(client).to receive(:get_user)

    client
  end
end

However, since I use this client in many services and rake tasks, I don't want to always keep mocking and stubbing it in every spec files I write. Where can I move my method #mock_client so that I can call it in any spec file? Thanks in advance!


Solution

  • In RSpec you can use shared contexts to share your test dependencies (let, let!) or test setup. This is basically a block thats evaluated in the context of the example group its included in:

    RSpec.shared_context "Discord mocks" do
      let(:client) { instance_double(DiscordClient) }
    
      before do
        allow(DiscordClient).to receive(:new).and_return(client)
        allow(client).to receive(:get_guild)
        allow(client).to receive(:get_user) 
      end
    end
    

    These can either be included manually in indivual specs with include_context or through your spec setup. Shared contexts are typically placed somewhere in /spec/support.

    On a side note you can decrease the need of stubbing in the first place by providing factory methods which should be used instead of new.get_guild if you don't need separate initialization and "call" arguments:

    class DiscordClient
      def self.get_guild(...)
        new.get_guild(...)
      end
    end
    

    Then all you need to do is stub the class methods that your client exposes:

    allow(DiscordClient).to receive(:get_guild)
    

    You'll find this pattern used extensively in Service Objects.