Search code examples
ruby-on-railsrubyrspecrspec-railsrspec-mocks

How to test if a class receives a message in RSpec?


I have a service object something like:

class SecurityQueryService
  class Contract::NotFound < StandardError; end

  attr_reader :ticker_name, :contract_type, :contract

  def initialize(user, params)
    @user          = user
    @ticker_name   = params[:ticker_name].upcase
    @contract_type = params[:type]
  end

  def call
    find_contract
  end

  private

  def find_contract
    @contract ||= contract_klass.find_by(ticker: ticker_name)
    fail(
      Contract::NotFound,
      "Cannot find contract with ticker_name #{ticker_name}"
    ) if @contract.nil?
  end

  def contract_klass
    return EquityContract if contract_type.nil?
    "#{contract_type.strip.capitalize}Contract".constantize
  end

end

And I have the following related spec:

require 'spec_helper'

describe SecurityQueryService do
  let(:user) { create(:customer) }
  let!(:equity_contract) { create(:equity_contract) }

  describe "#call" do
    describe "#find_contract" do
      it "returns contract based on contract type." do
        service = SecurityQueryService.new(user, {
          type: 'equity',
          ticker_name: equity_contract.ticker
        })
        service.call
        expect(EquityContract).to receive(:find_by)
      end    
    end
  end

end

I want to make sure EquityContract receives a find_by message whenever I call #call.

At the moment when I run the spec I get:

 Failure/Error: expect(EquityContract).to receive(:find_by)
   (EquityContract(id: integer, ticker: string, name: string, country: string, currency: string, instrument_type: string, bloomberg_ticker: string, benchmark_index: string, skip_valuation: boolean, isin: string, currency_factor: decimal, daily_update: boolean, id_bb_unique: string, image: string, domain: string) (class)).find_by(*(any args))
       expected: 1 time with any arguments
       received: 0 times with any arguments

How can I test this behaviour?


Solution

  • You need to set up the mock before you call the method. RSpec provides two ways to do this:

    1. Move the expectation before service.call:

      describe "#find_contract" do
        it "returns contract based on contract type." do
          service = SecurityQueryService.new(user,
            type: 'equity',
            ticker_name: equity_contract.ticker
          )
          expect(EquityContract).to receive(:find_by)
          service.call
        end    
      end
      
    2. Set up the method as a spy by allowing the method to be called, then expect it to have been called after the fact:

      describe "#find_contract" do
        it "returns contract based on contract type." do
          service = SecurityQueryService.new(user,
            type: 'equity',
            ticker_name: equity_contract.ticker
          )
          allow(EquityContract).to receive(:find_by)
          service.call
          expect(EquityContract).to have_received(:find_by)
        end    
      end
      

    As you can see, the first method requires the least typing, but requires awkward thinking ahead. The second method is more verbose, but is more logical in that it puts the expectation after the method call.