Search code examples
rubytestingrspectddrspec3

ruby rspec mocks test function receiving a hash


I am toying with the mailgun API, all functionalities seem to work now and I want to test them using rspec mocks.

I made some json fixtures under the rspec/fixtures folder, each has a json representing the expected result when I call a particular function. I also made a small helper:

module TestHelpers
  def self.get_json(filename:)
    JSON.parse File.read(filename)
  end
end

What I want to test is this function:

def self.get_messages_for(email:)
    sent_emails = []
    delivered_events = get_events_for(email: email)

    # :Accept => "message/rfc2822" will help us to get the raw MIME
    delivered_events.each do |event|
      response = RestClient::Request.execute method: :get ,
                                              url: event["storage"]["url"],
                                              user: "api", password:"#{configuration.api_key}",
                                              Accept: "message/rfc2822"
      sent_emails.push(JSON.parse(response))
    end
    sent_emails
  end

Which uses this helper to fetch events:

def self.get_events_for(email:, event_type: "delivered")
    delivered_to_target = []
    response = RestClient.get "https://api:#{configuration.api_key}"\
    "@api.mailgun.net/v3/#{configuration.api_domain}/events",
    :params => {
      :"event" => event_type
    }

    all_delivered = JSON.parse(response)["items"]

    all_delivered.each do |delivered|
      if (delivered.has_key?("recipients") and delivered["recipients"].include?(email)) or
        (delivered.has_key?("recipient") and delivered["recipient"].include?(email))
        delivered_to_target.push(delivered)
      end
    end
    delivered_to_target
  end

And here in my spec I have:

it 'can get the list of previously sent emails to an email address' do

    allow(StudySoup).to receive(:get_events_for).with({email: email}) {
      Array(TestHelpers::get_json(filename: 'spec/fixtures/events.json'))
    }

    allow(RestClient::Request).to receive(:execute).with(any_args){
      TestHelpers::get_json(filename: 'spec/fixtures/messages.json')
    }

    expect(StudySoup.get_messages_for(email: email)["subject"]).not_to be nil
  end

However, when I tried to run rspec, it always has the following fail trace:

1) StudySoup can get the list of previously sent emails to an email address
     Failure/Error: url: event["storage"]["url"],

     TypeError:
       no implicit conversion of String into Integer
     # ./lib/StudySoup.rb:51:in `[]'
     # ./lib/StudySoup.rb:51:in `block in get_messages_for'
     # ./lib/StudySoup.rb:49:in `each'
     # ./lib/StudySoup.rb:49:in `get_messages_for'
     # ./spec/StudySoup_spec.rb:86:in `block (2 levels) in <top (required)>'

I thought I stubbed out the RestClient::Request.execute method so it should work but it didn't. Any ideas on how I can correctly test this function? I tried to put many params matcher like anything(), hash_including(:key => value)... but it still didn't work.


Solution

  • You have indeed stubbed out the execute method. This means that rspec fiddles with that class so that attempts to call execute call rspec provided code instead of the original inplementation. In particular, all the arguments to that call are evaluated as normal.

    Changing the argument matchers as you have tried changes whether rspec decides the method call matches one of the configured stubs, but can't avoid the fact that evaluating event["storage"]["url"] is raising an exception.

    When you stubbed get_events_for you returned an array of arrays instead of an array of hashes: Array calls to_ary or to_a on its arguments which is turning your hash into an array of key value pairs, rather than wrapping the hash in an array as I think you thought it did.