0

When using the mandril-api the Rails Action Mailer is bypassed, so it is not possible to do something like this

it 'sends an email' do
   expect { subject.send_instructions }
     .to change { ActionMailer::Base.deliveries.count }.by(1)
end

I am trying to use an object_double to test my mailers. What I am trying to test is exactly what parameters are sent to the API (via an options hash).

So far, I have the Mandrill code here

  MANDRILL.messages.send_template( options[:template], [], message) unless Rails.env.staging?

Where MANDRILL is just the connection to the API, as detailed below.

describe 'verify content' do
  it 'uses the correct template' do
    api = object_double(Mandrill::API.new(ENV['MANDRILL_KEY']).messages)
    allow(api).to receive(:send_template)
    PostNotificationMailer.format_options(participant, post)
    expect(api).to have_received(:send_template)
    #expect(options[:template]).to eq('crowdai_standard_template')
  end
end

I'd like to be able to check all the params passed to the Mandrill API here. I can mock the messages method but not the messages.send_template

1) PostNotificationMailer verify content uses the correct template
   Failure/Error: expect(api).to have_received(:send_template)

   (ObjectDouble(#<Mandrill::Messages:0x007f8debd4f348 @master=#<Mandrill::API:0x007f8debd4faf0 @host="https://mandrillapp.com", @path="/api/1.0/", @session=#<Excon::Connection:7f8debd4f758 @data={:chunk_size=>1048576, :ciphers=>"HIGH:!SSLv2:!aNULL:!eNULL:!3DES", :connect_timeout=>60, :debug_request=>false, :debug_response=>false, :headers=>{"User-Agent"=>"excon/0.51.0"}, :idempotent=>false, :instrumentor_name=>"excon", :middlewares=>[Excon::Middleware::Hijack, Excon::Middleware::ResponseParser, Excon::Middleware::Expects, Excon::Middleware::Idempotent, Excon::Middleware::Instrumentor, Excon::Middleware::Mock], :mock=>false, :nonblock=>true, :omit_default_port=>false, :persistent=>false, :read_timeout=>60, :retry_limit=>4, :ssl_verify_peer=>true, :ssl_uri_schemes=>["https"], :stubs=>:global, :tcp_nodelay=>false, :thread_safe_sockets=>true, :uri_parser=>URI, :versions=>"excon/0.51.0 (x86_64-darwin15) ruby/2.3.1", :write_timeout=>60, :host=>"mandrillapp.com", :hostname=>"mandrillapp.com", :path=>"", :port=>443, :query=>nil, :scheme=>"https"} @socket_key="https://mandrillapp.com:443">, @debug=false, @apikey="redacted">>) (anonymous)).send_template(*(any args))
       expected: 1 time with any arguments
       received: 0 times with any arguments
 # ./spec/mailers/post_notification_mailer_spec.rb:14:in `block (3 levels) in <top (required)>'

** EDIT **

There is a gem MandrillMailer which solves the problem of testing against the Mandril API, but it's build is broken and it also seems to rebuild the API internally.

How do I test mandrill api with rspec

I couldn't find any tutorials or clear examples on how to use object_double.

Community
  • 1
  • 1
port5432
  • 5,889
  • 10
  • 60
  • 97
  • There are really 2 parts to this: how to intercept the options hash sent to the Mandrill API during tests, and how to use the object_double in Rspec – port5432 Nov 19 '16 at 15:24
  • pls, paste `subject.send_instructions` content method in the topic also – itsnikolay Nov 25 '16 at 10:59

2 Answers2

1

Have you thought about using the VCR gem ( https://github.com/vcr/vcr ) to record the response from the API call to mandrill into a fixture? Once the request is recorded, you can assert the values on the response to verify the expected data was passed.

Joey
  • 752
  • 1
  • 9
  • 26
  • Hi @Joey I have the VCR gem. I wanted to learn how to use object_doubles as there are few examples on them. I also wanted to trap what my application is sending to the API, without invoking it. There is a gem MandrillMailer but it seems to rebuild the API internally. http://stackoverflow.com/questions/19508030/how-do-i-test-mandrill-api-with-rspec – port5432 Nov 22 '16 at 07:37
  • With VCR, you are only making the API request the first time you run the test. Each consecutive time, the `cassette` is replayed and the remote API request is not made. Using the `mandrillmailer` gem as an opportunity to learn object_doubles seems like a bad use-case considering it rebuilds the API internally. With the API's not being public, you are going to run into inconsistencies as the gem's internal API's change. – Joey Nov 23 '16 at 03:19
1

As best as I can tell from your code, PostNotificationMailer.format_options(participant, post) has no way to know that its code is supposed to be sending the send_template method to your double instead of the predefined MANDRILL.messages object. If you call Mandrill::API.new(ENV['MANDRILL_KEY']) in your test, that returns a completely different object from MANDRILL even if you defined MANDRILL with the exact same code. So when the mailer sends the method to MANDRILL.messages, your double is oblivious.

Unfortunately, even if your test was rewritten to make the double based on MANDRILL.messages, it still wouldn't be the same object as what's in your mailer, because the mailer is calling the real MANDRILL.messages and not your double. The way I understand it, for most doubles you still have to use dependency injection. That is, your mailer would have to be set up so that you could set a parameter that would be "the object that does the mailing," something like (I'm making this up) PostNotificationMailer.set_api(some_object). In production, it would be PostNotificationMailer.set_api(MANDRILL), while in your test it would be PostNotificationMailer.set_api(api). Possibly that's more trouble than it's worth.

This seems to be confirmed by the object_double documentation, where the test includes:

user = object_double(User.new, :save => true)
expect(save_user(user)).to eq("saved!")

As you can see, the user object is passed as a parameter into the method that we're trying to test so that methods are called on the double and not some other object.

RSpec does seem to have the interesting ability to use object doubles on constants, so that you don't have to use dependency injection. However, based on the relevant documentation, it looks like you have to pass the object name as a string (not the actual object reference) and then you have to call as_stubbed_const on the double:

logger = object_double("MyApp::LOGGER", :info => nil).as_stubbed_const
Email.send_to('hello@foo.com')
expect(logger).to have_received(:info).with("Sent to hello@foo.com")

So maybe if your application defined a constant for the API's messages object, and then passed in its name as a string and called as_stubbed_const, it would work. I haven't tried using RSpec's doubles like this, so I can't say for sure.

Max
  • 1,817
  • 1
  • 10
  • 13