2

I am mixing a module into a mailer and adding it as a helper so it is accessible in the view. I need to test that the right helper method is being called from the view (so that a tracking pixel is included in an email), but Rspec doesn't seem to work:

require "spec_helper"

describe DeviseOverrideMailer do

  before :each do
    # Make the helper accessible.

    # This approach does not work
    # class MixpanelExposer; include MixpanelFacade end
    # @mixpanel = MixpanelExposer.new

    # This approach also does not seem to work, but was recommended here: http://stackoverflow.com/questions/10537932/unable-to-stub-helper-method-with-rspec
    @mixpanel = Object.new.extend MixpanelFacade
  end

  describe "confirmation instructions" do
    it "tells Mixpanel" do
      # Neither of these work.
      # DeviseOverrideMailer.stub(:track_confirmation_email).and_return('') 
      @mixpanel.should_receive(:track_confirmation_email).and_return('')

      @sender = create(:confirmed_user)
      @email = DeviseOverrideMailer.confirmation_instructions(@sender).deliver

    end
  end
end

The mailer:

class DeviseOverrideMailer < Devise::Mailer
  include MixpanelFacade
  helper MixpanelFacade
end

The Module:

class MixpanelFacade
  def track_confirmation_email
    # Stuff to initialise a Mixpanel connection
    # Stuff to add the pixel
  end
end

The mailer view (HAML):

-# Other HTML content

-# Mixpanel pixel based event tracking
- if should_send_pixel_to_mixpanel?
  = track_confirmation_email @resource

The error: It complains that it can't initialise the Mixpanel connection properly (because the request helper is missing), which shows that .should_receive() is not correctly stubbing the track_confirmation_email() method out. How can I get it to stub out correctly?

Matt Gibson
  • 14,616
  • 7
  • 47
  • 79
  • It would be helpful to see how `DeviseOverrideMailer` invokes the `track_confirmation_email` method. – exbinary Feb 05 '13 at 13:35
  • @an4rcho Edited to add the mailer view. – Matt Gibson Feb 05 '13 at 14:02
  • Matt, i'm not sure i understand what your goal is. You say 'I need to test that the right helper method is being called from the view..' which would indicate to me an expectation in a **view** spec, but the code above is trying to stub the helper in the spec for the **Mailer**. It's been a while since i looked at rails/rspec-rails so i might be missing the point here, but if `track_confirmation_email` is invoked in the view, then stubbing it would happen in the view spec, no? – exbinary Feb 05 '13 at 14:17
  • Perhaps I'm taking the wrong approach, but I've been using the email-spec gem to test the contents of the emails in the mailer spec. I have not made separate view specs for the mailer views. Is this possible/desirable? – Matt Gibson Feb 05 '13 at 14:53
  • I know it's been many days, but did you figure this out? I finally had a chance to look into this a bit but didn't get far (rails is really not designed with testing in mind!). I did post a hacky workaround below, FWIW. As far as your approach goes, i think you're right - i was applying the same thinking i would to controller specs but that probably doesn't make sense here. Morever, it appears from Dave Chelimsky's comments [here](https://groups.google.com/forum/#!topic/rspec/tItBKmDA7no/discussion) that this spec'ing w/o rendering isn't supported. – exbinary Feb 12 '13 at 16:22

2 Answers2

1

Rails makes this hard by not exposing an instance of Mailer. Notice how we define instance methods on Mailers, such as def confirmation_instructions(sender) ... but we call them as class methods like so: DeviseOverrideMailer.confirmation_instructions(@sender). This is made possible by some method_missing magic:

# actionmailer-3.2.11/lib/action_mailer/base.rb 
module ActionMailer
  #...
  class Base < AbstractController::Base
    #...
    class << self
      #...
      #line 437
      def method_missing(method, *args) #:nodoc:
        return super unless respond_to?(method)
        new(method, *args).message
      end

Notice the throwaway instance created by new(...).message. Our Mailer is instantiated, used and thrown away, giving us no easy way to intercept it from our specs for mocking/stubbing.

The only thing i can suggest is to extract the behavior you want to stub into a separate class method and stub that.

# in the helper:
module MixpanelFacade
  def track_confirmation_email
    Utils.track_confirmation_email(@some_state)
  end

  module Utils
    def self.track_confirmation_email(some_param)
      # Stuff to initialise a Mixpanel connection
      # Stuff to add the pixel
    end
  end
end

# in the spec
it "tells Mixpanel" do
  MaxpanelFacade::Utils.stub(:track_confirmation_email).and_return('') 
  @sender = create(:confirmed_user)
  @email = DeviseOverrideMailer.confirmation_instructions(@sender).deliver
end

It's certainly a hack -- we're extracting an unnecessary class method just so we can stub it out -- but i haven't run across any other way to do it. If you haven't solved this yet it'd be worth asking on the rspec mailing list (and please let me know what they say :).

exbinary
  • 1,086
  • 6
  • 8
0

So I'v found a better solution to this if you're using later versions of rspec.

You can just modify any_instance_of your mailer object and stub the specific methods that you want to stub.

any_instance_of(DeviseOverrideMailer) do |mailer| //do your should receieve here end

HaloZero
  • 475
  • 4
  • 15