Leonard Teo
Leonard Teo

Reputation: 1278

Rspec: ActionMailer::Base.deliveries is always empty

I've written an RSpec integration test. According to test.log, I can see that it has sent an email, but when I try to access the email using ActionMailer::Base.deliveries, it always shows it is empty.

I have read the other similar questions and tried everything. I'm stumped.

Here is the code using Capybara/RSpec:

it "should notify owner" do

  puts "Delivery method: #{ActionMailer::Base.delivery_method.inspect}"
  # This returns: ":test", which is correct

  # Get other guests to do the review
  @review = Review.last
  share_link = "http://#{@account.subdomain}.cozimo.local:#{Capybara.server_port}/review/#{@review.slug}"

  visit(share_link)

  fill_in "email", :with => @user1.email
  fill_in "password", :with => "foobar"

  click_button "Start Review"

  # Add a comment
  click_on "ice-global-note-button"
  find(:css, "#ice-global-note-panel > textarea.ui-corner-all").set "Foo"
  click_on "ice-global-note-submit-button"

  # Test is failing here. WTF? The array should not be empty
  ActionMailer::Base.deliveries.empty?.should be_false

  # Check that a notification email was sent to the owner
  open_email(@owner.email)
  current_email.should have_content "Hi #{@owner.first_name}"

end

As you can see above, config.action_mailer.delivery_method = :test

In test.log, it shows that the email really is sent!

Sent mail to [email protected] (62ms)
Date: Tue, 29 Jan 2013 13:53:48 -0500
From: Review Studio <[email protected]>
To: [email protected]
Message-ID: <51081abcd9fbf_5bd23fef87b264a08066@Leonards-MacBook-Pro.local.mail>
Subject: Notes added for Test
Mime-Version: 1.0
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

Hi Bruce,

Just letting you know that you have new notes added to the review:

  Project: Test
  Description: Test description
  URL: http://ballistiq.cozimo.local:3000/review/625682740

Thanks,

Review Studio
Completed 200 OK in 170ms (Views: 0.2ms | ActiveRecord: 4.3ms)

Upvotes: 8

Views: 5951

Answers (6)

Albert Catal&#224;
Albert Catal&#224;

Reputation: 2044

I had the same problem, and finally I found an easy solution, not sure if is the best way: All appreciations or corrections are welcome.

First of all adding this line in environtments/test.rb config.active_job.queue_adapter = :inline, this make execute a "deliver_now" even though we put in our code deliver_later

In the test add an expectation for the email message (I allways show a message telling that an email was sent, I guess that you too), for example:

expect(page).to have_content("We have sent a confirmation e-mail")
expect(ActionMailer::Base.deliveries.count).to eq(1)

This first expectation will wait untill 2 seconds (is the default timeout, eventhough is configurable via Capybara.default_wait_time = 2s).

So, in my case, putting this expectation and adding this config line in config/environments/test.rb is enough for passing the test correctly

Upvotes: 1

Ali Sepehri.Kh
Ali Sepehri.Kh

Reputation: 2508

In my case config.action_mailer.perform_deliveries was false for test environments.

Upvotes: 3

Leonard Teo
Leonard Teo

Reputation: 1278

This is an integration test using Capybara and Selenium. Therefore, you have to wait for the application to actually send the mail before checking that it has sent it.

Note - this solves the problem but is generally bad practice

Add a sleep 1 to tell rspec to wait after triggering the send mail event. It then resumes by checking the ActionMailer::Base.deliveries array and passed.

As mentioned, this is generally bad practice because it slows down tests.

Better way

Integration test shouldn't test the mail is sent at all. Tests should be divided up into clear responsibilities for the class being tested. Therefore, we'd structure the tests differently so that we only test for the mail being sent in another class (a controller or resource test). We could also use expectations to check that the call to the mail method was actually made though it's possible that we'd still get timing issues.

Upvotes: 7

ryanjones
ryanjones

Reputation: 5511

I think in general you want to avoid sleep() in your tests. If you're doing an integration test with Capybara you just need to use a method that waits.

For example, this test fails intermittently (valid_step_7 sends an email):

it "should get past step 7 and send an email" do
  valid_step_5_no_children
  valid_step_7

  expect(ActionMailer::Base.deliveries.count).to eq(1)
  expect(page).to have_content "I AM JOHN DOE"
end

This is because it's finishing the last step (valid_step_7) and instantly checking 'ActionMailer::Base.deliveries.count' to see if it's equal to 1, but it hasn't necessarily hasn't had time to populate the deliveries array (from my debugging anyways).

I haven't had random failures since I flipped my test to first check the content on the next page, give the time for ActionMailer::Base.deliveries to be populated and perform the check:

it "should get past step 7 and send an email" do
  valid_step_5_no_children
  valid_step_7

  expect(page).to have_content "I AM JOHN DOE"
  expect(ActionMailer::Base.deliveries.count).to eq(1)
end

I suspect sleep(1) will work, but I think you're forcing a 1s sleep when it's not needed.

Upvotes: 0

chwongris
chwongris

Reputation: 1

You could also use this wait_for_ajax helper instead of sleep for your javascript POST. It will make Capybara wait for the ajax to finish.

module WaitForAjax
  def wait_for_ajax
   Timeout.timeout(Capybara.default_wait_time) do
    loop until finished_all_ajax_requests?
   end
  end

  def finished_all_ajax_requests?
   page.evaluate_script('jQuery.active').zero?
  end
end

RSpec.configure do |config|
  config.include WaitForAjax, type: :feature
end

Source: https://robots.thoughtbot.com/automatically-wait-for-ajax-with-capybara

Upvotes: 0

arieljuod
arieljuod

Reputation: 15838

You should never use sleep or those time consuming methods on the specs, i't will only slow down your specs!

Try using an expectation on your mailer, at the beginning of your test add something like

mail = mock(mail)
mail.should_receive(:deliver)
YourMailer.should_receive(:your_method).once.and_return(mail)

that way you don't have to wait and you are actually testing what you have to test (that the code creates and delivers the mail) and not the mailer code (you only call deliver on a mail object, the actual delivery is a job of the ActionMailer tests and you have nothing to do with it on your application, you should just trust that calling those method works)

Upvotes: 6

Related Questions