Allowing Unit Tests to Work as Intended

Several months ago, I had the opportunity to improve part of a testing suite for an application I worked on. Specifically, I was improving some unit testing pertaining to the creation of DelayedJob entries when emailing users.

The code being tested related to users being emailed upon registration on the site.

def User < ActiveRecord::Base
  after_create :send_welcome_email
 
  def send_welcome_email
    UserMailer.welcome(email).deliver
  end
  handle_asynchronously :send_welcome_email
end

The test’s original implementation compared the number of delayed jobs being created to the number of expected delayed job entries. The unit test code essentially looked like this.

class UserTest < Test::Unit::TestCase
  context "when creating a new user" do
    setup do
      @user = Factory.create(:user)
    end
 
    should "send new user a welcome email" do
      assert_equal 1, Delayed::Job.count
    end
  end
end

Since the email is sent via a delayed job, its implementation for success is to compare the number of delayed jobs to the number of expected delayed jobs (in this case, just one). Everything works, and the test passes successfully. That’s great for this particular instance.

But what happens if we change the User model’s code to the following?

def User < ActiveRecord::Base
  after_create :send_notification_to_referring_user
 
  def send_notification_to_referring_user
    UserMailer.referral(referrer.email).deliver if referrer
  end
  handle_asynchronously :send_notification_to_referring_user
end

When we run our unit test again, it still passes. However, the unit test is supposed to be ensuring the user receives a welcome email, given the line of code reading should "send new user a welcome email". Clearly given the updated model code, that is no longer happening.

An improvement for our test would be to check that the correct method is being called via Delayed Job.

assert_delayed_method :send_notifications

There is no assert_delayed_method supplied by default, but that doesn’t mean we can’t create one. Within test/test_helper.rb, let’s go ahead and add the following code.

def assert_delayed_method(method_name, msg = nil)
  method_name = method_name.to_sym
  full_message = build_message(msg, "Delayed Job should exist for ?.", method_name)
 
  assert_block(full_message) do
    match = false
    Delayed::Job.find_each do |job|
      match = true and break if job.payload_object.method == method_name
    end
    match
  end
end

Now, let’s change our unit test to take advantage of this new assertion method.

class UserTest < Test::Unit::TestCase
  context "when creating a new user" do
    setup do
      @user = Factory.create(:user)
    end
 
    should "send new user a welcome email" do
      assert_delayed_method :send_notification_to_referring_user
    end
  end
end

The unit test still passes, and we can rest assured that if a different delayed job is created in the future, our test will fail.

Leave a Reply

Your email address will not be published. Required fields are marked *