20

How do I test that a certain instance variable is set in my my mailer with rspec? assigns is coming back undefined..

require File.dirname(__FILE__) + '/../../spec_helper'

describe UserMailer do

  it "should send the member user password to a User" do
    user = FG.create :user

    user.create_reset_code

    mail = UserMailer.reset_notification(user).deliver

    ActionMailer::Base.deliveries.size.should == 1  

    user.login.should be_present  

    assigns[:person].should == user
    assigns(:person).should == user #both assigns types fail
  end
end

The error returned is:

undefined local variable or method `assigns' for #<RSpec::Core::ExampleGroup::Nested_1:0x007fe2b88e2928>
pixelearth
  • 13,674
  • 10
  • 62
  • 110

2 Answers2

22

assigns is only defined for controller specs and that's done via the rspec-rails gem. There is no general mechanism to test instance variables in RSpec, but you can use Kernel's instance_variable_get to access any instance variable you want.

So in your case, if object were the object whose instance variable you were interested in checking, you could write:

expect(object.instance_variable_get(:@person)).to eql(user)

As for getting ahold of the UserMailer instance, I can't see any way to do that. Looking at the method_missing definition inside https://github.com/rails/rails/blob/master/actionmailer/lib/action_mailer/base.rb, a new mailer instance will be created whenever an undefined class method is called with the same name as an instance method. But that instance isn't saved anywhere that I can see and only the value of .message is returned. Here is the relevant code as currently defined on github:

Class methods:

  def respond_to?(method, include_private = false) #:nodoc:
    super || action_methods.include?(method.to_s)
  end

  def method_missing(method_name, *args) # :nodoc:
    if respond_to?(method_name)
      new(method_name, *args).message
    else
      super
    end
  end

Instance methods:

attr_internal :message

# Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
# will be initialized according to the named method. If not, the mailer will
# remain uninitialized (useful when you only need to invoke the "receive"
# method, for instance).
def initialize(method_name=nil, *args)
  super()
  @_mail_was_called = false
  @_message = Mail.new
  process(method_name, *args) if method_name
end

def process(method_name, *args) #:nodoc:
  payload = {
    mailer: self.class.name,
    action: method_name
  }

  ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
    lookup_context.skip_default_locale!

    super
    @_message = NullMail.new unless @_mail_was_called
  end
end
Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106
  • 1
    so rspec rails doesn't allow you to test instance vars in mailers? Minitest does... – pixelearth Jan 12 '14 at 21:35
  • That's useful, except in this case, I'm not sure what object would have those instance vars set. Any ideas? – pixelearth Jan 13 '14 at 02:33
  • I'm not sure. Usually when you're checking the value of an instance variable, you're looking at some code in particular. What code we're you trying to test value of `@person` for? – Peter Alfvin Jan 13 '14 at 06:10
  • It's defined in the mailer method and accessible to the view template that is used to generate the mail. – pixelearth Jan 13 '14 at 11:34
  • 1
    See updated message for how I've hit the end of my rope on this. ;-) – Peter Alfvin Jan 13 '14 at 15:40
  • Thanks for your input @Peter. I wonder how MiniTest / Test::Unit manages this. – pixelearth Jan 13 '14 at 21:26
  • I'm pretty sure there's nothing in MiniTest/Test::Unit that makes this any easier. – Peter Alfvin Jan 13 '14 at 21:47
  • In the rails IRC channel, there was a comment that testing these variables could be testing the "implementation" rather than the "interface", anyway. I think that could go either way, though. – pixelearth Jan 14 '14 at 03:36
3

I don't think this is possible to test unless Rails changes its implementation so that it actually provides access to the ActionMailer (controller) object and not just the Mail object that is generated.

As Peter Alfvin pointed out, the problem is that it returns the 'message' here:

new(method_name, *args).message

instead of just returning the mailer (controller) like this:

new(method_name, *args)

This post on the rspec-rails list might also be helpful:

Seems reasonable, but unlikely to change. Here's why. rspec-rails provides wrappers around test classes provided by rails. Rails functional tests support the three questions you pose above, but rails mailer tests are different. From http://guides.rubyonrails.org/action_mailer_basics.html: "Testing mailers normally involves two things: One is that the mail was queued, and the other one that the email is correct."

To support what you'd like to see in mailer specs, rspec-rails would have to provide it's own ExampleGroup (rather than wrap the rails class), which would have to be tightly bound to rails' internals. I took great pains in rspec-rails-2 to constrain coupling to public APIs, and this has had a big payoff: we've only had one case where a rails 3.x release required a release of rspec-rails (i.e. there was a breaking change). With rails-2, pretty much every release broke rspec-rails because rspec-rails was tied to internals (rspec-rails' fault, not rails).

If you really want to see this change, you'll need to get it changed in rails itself, at which point rspec-rails will happily wrap the new and improved MailerTestCase.

Tyler Rick
  • 9,191
  • 6
  • 60
  • 60