cult3

Testing Active Job and Active Mailer in Ruby on Rails

Mar 09, 2016

Table of contents:

  1. Unit Testing the Mailer
  2. Action Mailer Previews
  3. Functional Testing the Mailer
  4. Unit Testing Jobs
  5. Functional Testing Jobs
  6. Conclusion

Over the last couple of weeks we’ve looked at getting started with Active Job and Active Mailer.

Being able to send email and put work onto a queue are fairly critical components of a modern web application, and so Rails makes a nice abstraction so we can get on with the important parts of the application.

One thing we haven’t covered so far is testing Active Job and Active Mailer.

When something is a little bit different, it’s tempting to just not write tests for it.

But at some point in the future that attitude is going to come back around and bite you.

In today’s tutorial we will be looking at testing Active Job and Active Mailer in Ruby on Rails.

Unit Testing the Mailer

The purpose of a Unit Test is to execute code in a controlled environment with controlled inputs and then assert that the output is correct.

If you were following along with Getting started with Action Mailer in Ruby on Rails and you used the Rails generator to create the UserMailer you should also have a user_mailer_test.rb file under the test/mailers directory:

require 'test_helper'

class UserMailerTest < ActionMailer::TestCase
  # test "the truth" do
  # assert true
  # end
end

Here we can write the test for the sign_up_confirmation:

require 'test_helper'

class UserMailerTest < ActionMailer::TestCase
  test 'sign up confirmation' do
  end
end

First I will invoke the sign_up_confirmation method on the UserMailer class by passing it a new User and then calling the deliver_now method to send the email:

email = UserMailer.sign_up_confirmation(user = create(:user)).deliver_now

Now that the email has been sent, we can make the assertions to ensure everything has worked as it should.

When Active Mailer sends an email during your tests, instead of actually send the email it will simply append it to a deliveries array.

So first we can assert that the deliveries array is not empty:

assert_not ActionMailer::Base.deliveries.empty?

Next we can make some assertions on the email that was returned from the deliver_now method:

assert_equal ['from@example.com'], email.from
assert_equal [user.email], email.to
assert_equal 'Confirm your Culttt Membership!', email.subject

Here I’m simply asserting that the to and from email addresses and the subject are correct.

Here is that test in full:

require 'test_helper'

class UserMailerTest < ActionMailer::TestCase
  test 'sign up confirmation' do
    email = UserMailer.sign_up_confirmation(user = create(:user)).deliver_now

    assert_not ActionMailer::Base.deliveries.empty?

    assert_equal ['from@example.com'], email.from
    assert_equal [user.email], email.to
    assert_equal 'Confirm your Culttt Membership!', email.subject
  end
end

Another aspect of Unit Testing Mailers that you often hear about is asserting that the content of the email is correct.

Personally I’m not a big fan of this practice. Emails have a tendency to change and so it makes writing tests brittle. I would rather just do a manual check whenever an email is changed instead of trying to maintain brittle tests.

Action Mailer Previews

Manually testing that an email looks correct can be a bit of a pain in the arse.

Normally you would have to trigger the email and then wait for it to hit your inbox. This is especially annoying when you are making lots of little tweaks as the feedback loop is very slow.

However, fortunately for us Rails has already solved this problem by allowing you to very quickly render the email in the browser.

When you generated the UserMailer you might have also noticed that a UserMailerPreview class was generated under the test/mailers/previews directory:

class UserMailerPreview < ActionMailer::Preview
end

In this class we can set up the email so we can preview the output in the browser:

class UserMailerPreview < ActionMailer::Preview
  def sign_up_confirmation
    user = User.first

    UserMailer.sign_up_confirmation(user)
  end
end

Now if you fire up Rails and go to https://localhost:3000/rails/mailers/user_mailer/sign_up_confirmation in your browser you should see the rendered email.

This makes it very easy to tweak the contents or the design of your email with that nice and quick feedback loop of testing in the browser.

Functional Testing the Mailer

With the Unit Test in place we can now turn our attention to writing Functional Tests.

Whereas a Unit Test asserts that our code is working correctly in isolation and with controlled inputs, a Functional Test should assert that our application is working correctly as a whole.

In this case this means, is the correct email being sent when the application receives a particular request.

Functional Tests should not be concerned with the actual sending of the email as that is the responsibility of the framework.

A simplified Functional Test might look something like this:

class UserControllerTest < ActionController::TestCase
  test 'register' do
    assert_difference 'ActionMailer::Base.deliveries.size', +1 do
      post :register, email: 'me@example.com'
    end

    email = ActionMailer::Base.deliveries.last

    assert_equal ['from@example.com'], email.from
    assert_equal ['me@example.com'], email.to
    assert_equal 'Confirm your Culttt Membership!', email.subject
  end
end

In this test I’m making a POST request to the UserController and then asserting that the correct email is sent.

The point of this test is to assert that my application’s business logic is working correctly. In this case, my business logic is dictating that a confirmation email should be sent to the user when they first register with the application.

Although this is a simple example, hopefully it illustrates the point. We’re going to be looking more in depth at Functional Testing in the coming weeks.

Unit Testing Jobs

When we first looked at Active Job we generated a Job for adding users to MailChimp as a queued processed. The generator should of also created a test class called add_user_to_mail_chimp_job_test.rb under the test/jobs directory.

Here is where you can Unit Test your Job:

require 'test_helper'

class AddUserToMailChimpJobTest < ActiveJob::TestCase
  # test "the truth" do
  # assert true
  # end
end

I’m not going to actually show the Unit Test for this Job as testing when hitting a third-party API is opening another can of worms, and so we can leave that for another day. The important thing is you know where to write your Unit Tests.

Functional Testing Jobs

Again, as with Mailer Functional Tests, you don’t care as much about the Job being processed, rather that the Job was queued in the first place.

To make writing these kinds of tests easier, Rails has a number of helper assertions for asserting that a Job was enqueued or performed.

By default these assertions will not be included in your TestCase, but you can add them by including this line:

include ActiveJob::TestHelper

You will now have access to the following assertions:

assert_enqueued_jobs(number)

Asserts that the number of enqueued jobs matches the given number.

assert_performed_jobs(number)

Asserts that the number of performed jobs matches the given number.

assert_no_enqueued_jobs { ... }

Asserts that no jobs have been enqueued.

assert_no_performed_jobs { ... }

Asserts that no jobs have been performed.

assert_enqueued_with([args]) { ... }

Asserts that the job passed in the block has been enqueued with the given arguments.

assert_performed_with([args]) { ... }

Asserts that the job passed in the block has been performed with the given arguments.

As I mentioned above, we will be looking at some more concrete examples of Functional Testing in Rails in the coming weeks, so don’t worry if what we’ve covered today is still over your head.

Conclusion

It’s really important to test your entire application, even the bits that are a bit different and outside of your comfort zone.

Not testing the awkward interactions with your application might be easier in the short term, but it will most likely come and bite you at some point in the future when something goes wrong and you don’t catch it before it’s deployed to production.

Rails actually gives you almost everything you need to write Unit and Functional tests for your Mailer and Job code and so there really isn’t an excuse for not writing these types of tests.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.