ActionMailer fails when testing mailer - ruby-on-rails

Rails 4.1.4 and Rspec 3
I'm doing a VERY basic email test with Rspec. If I call the mailer from rails console, it works perfectly. If I call it from the mailer spec I get:
wrong number of arguments (0 for 1..2)
The mailer is very basic:
def create_team_invite(org, email)
#organization = org
mail(:to=>email, :subject=>'Test Subject')
end
The Test is pretty basic too:
it 'can send out emails to invite others to create teams' do
UserMailer.create_team_invite(#org, 'test#test.com').deliver
expect(ActionMailer::Base.deliveries.count).to eq 1
mail = ActionMailer::Base.deliveries.first
expect(mail.subject).to eq 'Test Subject'
expect(mail.from).to eq 'test#test.com'
end
Its failing in the "mail(:to..." line in the mailer. Seems like maybe its some configuration issue in my environment, but I have Test setup exactly the same as Dev, using SMTP and sending it to a Mailcatcher port. I caught the exception and looked at the Backtrace, but don't see anything unusual...
Anyone seen this before?
Update: providing additional info that was requested.
My test.rb, minus comments:
Rails.application.configure do
config.cache_classes = true
config.eager_load = false
config.serve_static_assets = true
config.static_cache_control = 'public, max-age=3600'
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.action_dispatch.show_exceptions = false
config.action_controller.allow_forgery_protection = false
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.active_support.deprecation = :stderr
config.action_mailer.default_url_options = { :host => 'lvh.me:3000', :only_path=>false }
end
The entire rspec failure is:
UserMailer
Team Joining Email
can send out emails to invite others to create teams (FAILED - 1)
Failures:
1) UserMailer Team Joining Email can send out emails to invite others to create teams
Failure/Error: UserMailer.create_team_invite(#team, 'test#test.com').deliver
ArgumentError:
wrong number of arguments (0 for 1..2)
# ./app/mailers/user_mailer.rb:11:in `create_team_invite'
# ./spec/mailers/user_mailer_spec.rb:36:in `block (3 levels) in <top (required)>'
Finished in 0.25858 seconds (files took 29 minutes 48 seconds to load)
1 example, 1 failure
The way I configure my email is via an initializer that loads an email.yml file from my config, per environment. Exact same process used by both test and dev, with the same settings. (Again, I'm sending to Mailcatcher, instead of just to mail_delivery :test)
Update 2
I have narrowed it down to the Mailer missing the "request" object. If I dig through where the error is occurring (AbstractController rendering.rb, line 109) it tries to reference the request object:
if defined?(request) && request && request.variant.present?
This is calling over to Rack test.rb line 121:
def request(uri, env = {}, &block)
env = env_for(uri, env)
process_request(uri, env, &block)
end
So its like the Rack Test.rb class is being seen as the request method in that abstractcontroller... but I dont know how, or why, or why this is happening in this particular project...

I get this same exact error, and it seems that it's trying to call rack-test request code, when I'm just trying to test a mailer object. So what I did is just not include all the stuff that I put in the spec_helper for the mailer spec. That fixed the problem for me.
# require 'spec_helper' -> I removed this line,
# and manually require the files that's needed to just test the mailer object:
require 'rubygems'
require './config/environment'
require './app/mailers/your_mailer'
Note: I'm just doing a rack project using action_mailer, no rails stuff. So your solution will be different, but you get the idea.
Update:
After doing some more troubleshooting. I found this problem in my spec_helper.rb file
RSpec.configure do |config|
config.include include RackSpecHelper # notice the double include
...
end
# where RackSpecHelper is a custom module
module RackSpecHelper
include Rack::Test::Methods
# a bunch of other helper methods
end
Notice the double include on this line
config.include include RackSpecHelper
At first I tried just removing the line and my mailer test is running just fine. Suspicious with the double include, I remove the include so it's just like this
config.include RackSpecHelper
Now my mailer test runs just fine without having to do manual require like I posted earlier above (and it can run together with the other test that uses rack test stuff).
The double include is basically doing
config.include(include(RackSpecHelper))
which include the RackSpecHelper in the configure block, which loads all the methods in the top level namespace! (very bad thing). It works, but that means all the methods from Rack::Test::Methods are in the global namespace.
So I'm guessing in your case, you might have a line in the spec_helper that include Rack::Test::Methods in the global namespace like this
include Rack::Test::Methods
remove it and instead put it in the RSpec config like this
RSpec.configure do |config|
config.include RackSpecHelper
end

Related

How to avoid sending emails in Rails test-env with sendgrid-ruby?

Problem
I seem to be facing a stubborn issue with my RSpec tests trying to constantly send emails in test-env despite my configuration should avoid it. Whatever I try it seems to totally ignore it.
My environment
Rails 6.1.1
Ruby 3.0.0
sendgrid-ruby gem 6.3.9
I have a mailer class inheritance-chain as follows: OrganizationMailer<-ApplicationMailer<-ActionMailer::Base
In my config/environments/test.rb I have the following mail-related configuration
config.action_mailer.delivery_method = :test
config.action_mailer.perform_deliveries = false
config.active_job.queue_adapter = :test
My config/application.rb and config/environment.rb don't contain any extra configuration.
Bit offtopic maybe, but just in case adding it as well:
I have ensured that the line ENV['RAILS_ENV'] ||= 'test' is present in both- spec/spec_helper.rb and spec/rails_helper.rb since for some reason Rails randomly triggered my tests in staging environment. I also had to change the .rspec file contents in my project from --require rspec_helper to --require rails_helper. I got the solution from here1, here2 and here3. But this I don't think plays any role in current problem.
Bad solution
I hacked my way through this issue atm by just adding the unless Rails.env.test? on top of each email-sending method I'm having to ensure none of them reach Sendgrid in tests, but this sucks big time I know. That's why I'm posting this question to get this fixed properly without such if/unless-clauses.
My theory
Can it be that sendgrid-ruby is here to blame? I inherit things from ApplicationMailer and ActionMailer but eventually what is sending the emails is Sendgrid ruby gem. If so, how to avoid it best? I didn't find any hints from the sendgrid-ruby documentation about that. I will post one of my simple mailer-methods below so you could see the situation atm:
# frozen_string_literal: true
# using SendGrid's Ruby Library
# https://github.com/sendgrid/sendgrid-ruby
require 'sendgrid-ruby'
class MyMailer < ApplicationMailer
include SendGrid
def my_mailer_method(my_object:)
unless Rails.env.test? # <---- Hack I'd like to get rid of
from = Email.new(email: 'no-reply#my.domain', name: t('general.title'))
to = Email.new(email: my_object.contact_email)
subject = "#{t('my_mailer.my_mailer_method.subject')}: #{my_object.my_object_title}"
content = Content.new(
type: 'text/html',
value: ApplicationController.render(
template: 'my_mailer/my_mailer_method',
locals: {
my_object: my_object
},
layout: nil
)
)
mail = SendGrid::Mail.new(from, subject, to, content)
sg = SendGrid::API.new(api_key: ENV['SENDGRID_API_KEY'])
# Send out mail
response = sg.client.mail._('send').post(request_body: mail.to_json)
end
end
end
The issue here is that you may have set config.action_mailer.delivery_method = :test but you are not actually using ActionMailer to deliver your emails. Instead, within the MyMailer class you are directly using the SendGrid API, and sidestepping ActionMailer entirely.
If you want to use SendGrid API to send your emails, then I actually recommend using the sendgrid-actionmailer gem. It allows you to use ActionMailer to build your emails and uses the SendGrid API under the hood to send them. This allows you to send other parameters that the API supports and would be more difficult or impossible with SMTP, while still using the Rails standard ActionMailer to send the emails.
To ensure that your mails are sent by SendGrid in production, but not sent in test, you would set:
config.action_mailer.delivery_method = :sendgrid_actionmailer
in your production.rb environment. And set:
config.action_mailer.delivery_method = :test
config.action_mailer.perform_deliveries = false
as you already have in your test.rb environment.

Deliver later not working in the test environment in Rails 5

Basically, with the test config set up exactly how it worked in Rails 4 (delivery method set as test etc), aside from deprecated options which I have replaced, mail only sends with deliver_now, not deliver_later. Deliver_later works in the development environment, even when the config is identical between the two environments.
Test environment mailer config:
config.action_mailer.delivery_method = :test
config.action_mailer.perform_deliveries = true
config.action_mailer.perform_caching = false
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_url_options = { :host => 'localhost:3000' }
I have the same issue, and i have instead resorted to using assertions on
enqueued_jobs.size
to assert my mail has been queued. Additionally i have unit tests on the mailer class where i use deliver_now.
This requires including ActiveJob:TestHelper
class ActiveSupport::TestCase
include ActiveJob::TestHelper
end
Another option for testing emails that use deliver_later (Active Job) is to put the code you want executed now (not queued) and your assertions in a perform_enqueued_jobs block. This also requires adding an include at the top of your test file just below the class definition
include ActiveJob::TestHelper
Then something like the below.
perform_enqueued_jobs do
post article_url, params: { article: { title: "Learn Testing", body: "Lorem Ipsum" } }
assert_not_equal 0, ActionMailer::Base.deliveries.size
end

testing using Resque with Rspec examples?

I am processing my background jobs using Resque.
My model looks like this
class SomeClass
...
repo = Repo.find(params[:repo_id])
Resque.enqueue(ReopCleaner, repo.id)
...
end
class RepoCleaner
#queue = :repo_cleaner
def self.perform(repo_id)
puts "this must get printed in console"
repo = Repo.find(repo_id)
# some more action here
end
end
Now to test in synchronously i have added
Resque.inline = Rails.env.test?
in my config/initializers/resque.rb file
This was supposed to call #perform method inline without queuing it into Redis and without any Resque callbacks as Rails.env.test? returns true in test environment.
But
"this must get printed in console"
is never printed while testing. and my tests are also failing.
Is there any configurations that i have missed.
Currently i am using
resque (1.17.1)
resque_spec (0.7.0)
resque_unit (0.4.0)
I personally test my workers different. I use RSpec and for example in my user model I test something like this:
it "enqueue FooWorker#create_user" do
mock(Resque).enqueue(FooWorker, :create_user, user.id)
user.create_on_foo
end
Then I have a file called spec/workers/foo_worker_spec.rb with following content:
require 'spec_helper'
describe FooWorker do
describe "#perform" do
it "redirects to passed action" do
...
FooWorker.perform
...
end
end
end
Then your model/controller tests run faster and you don't have the dependency between model/controller and your worker in your tests. You also don't have to mock so much things in specs which don't have to do with the worker.
But if you wan't to do it like you mentioned, it worked for me some times ago. I put Resque.inline = true into my test environment config.
It looks like the question about logging never got answered. I ran into something similar to this and it was from not setting up the Resque logger. You can do something as simple as:
Resque.logger = Rails.logger
Or you can setup a separate log file by adding this to your /lib/tasks/resque.rake. When you run your worker it will write to /log/resque.log
Resque.before_fork = Proc.new {
ActiveRecord::Base.establish_connection
# Open the new separate log file
logfile = File.open(File.join(Rails.root, 'log', 'resque.log'), 'a')
# Activate file synchronization
logfile.sync = true
# Create a new buffered logger
Resque.logger = ActiveSupport::Logger.new(logfile)
Resque.logger.level = Logger::INFO
Resque.logger.info "Resque Logger Initialized!"
}
Mocking like daniel-spangenberg mentioned above ought to write to STDOUT unless your methods are in the "private" section of your class. That's tripped me up a couple times when writing rspec tests. ActionMailer requires it's own log setup too. I guess I've been expecting more convention than configuration. :)

Email spec doesn't match body content in Rails

I'm using email_spec gem to test a simple email, but for some reason the body content appears to be empty:
1) ContactMailer welcome email to new user renders the body
Failure/Error: mail.should have_body_text("Hi")
expected the body to contain "Hi" but was ""
# ./spec/mailers/contact_mailer_spec.rb:17:in `block (3 levels) in <top (required)>'
Every other example passes. The template file is called welcome_email.text.erb. Not sure why body is not matched, but the email does have a body when it gets sent.
Edit: the Rspec code is:
let(:mail) { ContactMailer.welcome_email(email) }
it "renders the body" do
mail.should have_body_text("Hi")
end
The best way I've found to do this is:
it "contains a greeting" do
mail.html_part.body.should match /Hi/
end
You can also use text_part in place of html_part if you want to check the plain text part of a multipart message.
Also note that others may recommend using #encoded, but I had trouble using that with long URLs, as they may get line-wrapped during the encoding process.
So, I was experiencing the same thing. I was trying to test my mailers without loading all of Rails.
What finally solved my problem was adding this to my test:
(note that my test is in test/unit/mailers/my_mailer_test.rb - you may have to adjust paths)
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.view_paths = File.expand_path('../../../../app/views', __FILE__)
Basically, without the view paths pointing to your views directory, the template is not found and all the parts (html, text, etc) are blank.
NOTE: The directory specified is NOT the one the actual templates are in. The mailer knows to look for a directory in the template root named after the class itself.
Here's a sample in minitest/spec
require 'minitest/spec'
require 'minitest/autorun'
require "minitest-matchers"
require 'action_mailer'
require "email_spec"
# NECESSARY TO RECOGNIZE HAML TEMPLATES
unless Object.const_defined? 'Rails'
require 'active_support/string_inquirer'
class Rails
def self.env
ActiveSupport::StringInquirer.new(ENV['RAILS_ENV'] || 'test')
end
end
require 'haml/util'
require "haml/template"
end
# END HAML SUPPORT STUFF
require File.expand_path('../../../../app/mailers/my_mailer', __FILE__)
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.view_paths = File.expand_path('../../../../app/views', __FILE__)
describe MyMailer do
include EmailSpec::Helpers
include EmailSpec::Matchers
let(:the_email){ MyMailer.some_mail() }
it "has the right bit of text" do
the_email.must have_body_text("some bit of text")
end
end

undefined method `find_link' for #<Cucumber::Rails::World:0x818e02e8> (NoMethodError)

Rspec obviously hates me. I kinda hate him back.
#features/step_definitions/custom_steps.rb
Then /^I should see the link "([^\"]*)"$/ do |linked_text|
find_link(linked_text)
end
#link.feature
Then I should see the link "foo"
From terminal:
undefined method `find_link' for #<Cucumber::Rails::World:0x818e02e8> (NoMethodError)
./features/step_definitions/custom_steps.rb:115:in `/^I should see the link "([^\"]*)"$/'
My env.rb file:
#features/support/env.rb
# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
# It is recommended to regenerate this file in the future when you upgrade to a
# newer version of cucumber-rails. Consider adding your own code to a new file
# instead of editing this one. Cucumber will automatically load all features/**/*.rb
# files.
ENV["RAILS_ENV"] ||= "cucumber"
require File.expand_path(File.dirname(__FILE__) + '/../../config/environment')
require 'cucumber/formatter/unicode' # Remove this line if you don't want Cucumber Unicode support
require 'cucumber/rails/world'
require 'cucumber/rails/active_record'
require 'cucumber/web/tableish'
require 'webrat'
require 'webrat/core/matchers'
require 'spec/stubs/cucumber'
Scenes::load
Webrat.configure do |config|
config.mode = :rails
config.open_error_files = false # Set to true if you want error pages to pop up in the browser
end
# If you set this to false, any error raised from within your app will bubble
# up to your step definition and out to cucumber unless you catch it somewhere
# on the way. You can make Rails rescue errors and render error pages on a
# per-scenario basis by tagging a scenario or feature with the #allow-rescue tag.
#
# If you set this to true, Rails will rescue all errors and render error
# pages, more or less in the same way your application would behave in the
# default production environment. It's not recommended to do this for all
# of your scenarios, as this makes it hard to discover errors in your application.
ActionController::Base.allow_rescue = false
# If you set this to true, each scenario will run in a database transaction.
# You can still turn off transactions on a per-scenario basis, simply tagging
# a feature or scenario with the #no-txn tag. If you are using Capybara,
# tagging with #culerity or #javascript will also turn transactions off.
#
# If you set this to false, transactions will be off for all scenarios,
# regardless of whether you use #no-txn or not.
#
# Beware that turning transactions off will leave data in your database
# after each scenario, which can lead to hard-to-debug failures in
# subsequent scenarios. If you do this, we recommend you create a Before
# block that will explicitly put your database in a known state.
Cucumber::Rails::World.use_transactional_fixtures = true
# How to clean your database when transactions are turned off. See
# http://github.com/bmabey/database_cleaner for more info.
if defined?(ActiveRecord::Base)
begin
require 'database_cleaner'
DatabaseCleaner.strategy = :truncation
rescue LoadError => ignore_if_database_cleaner_not_present
end
end
What is wrong? Thank you.
This error is telling you none of your steps or helpers define this method.
Are you trying to use one of the helpers buried in Webrat? It sounds like you want:
Webrat::Locators.find_link

Resources