Search code examples
ruby-on-railsrspecdelayed-jobrails-activejob

How to test an ActiveJob callback?


I'm in the process of building an email reminder feature for students. They should receive an email at an interval that their instructor specifies indicating how much progress they have made.

My approach is to use ActiveJob to schedule a job that sends an email, then when the job is performed, I use the after_perform callback to schedule the next job (taken from this post). Here's what that looks like:

class StudentReminderJob < ApplicationJob
  queue_as :default

  after_perform do |job|
    # (get user update frequency preference)

    self.class.set(wait: update_frequency).perform_later(job.arguments[0]) 
  end

  def perform(user)
    # Send email containing student stats information
    UserMailer.send_student_reminder_email(user)
  end
end

The initial set of reminders will be kicked off by a rake task.

My issue is this: when trying to test the above code with perform_enqueued_jobs I seem to be hitting an infinite loop. Here's my test:

require 'rails_helper'

describe StudentReminderJob, active_job: true, type: :job do
  include ActiveJob::TestHelper
  include ActiveSupport::Testing::TimeHelpers

  let(:user) { create :user }
  subject(:job) { described_class.perform_later(user) }

  it 'gets enqueued' do
    # Passes
    expect { job }.to have_enqueued_job(described_class)
  end

  it 'receives perform later with user and base URL' do
    # Passes (may be duplicatative)
    expect(StudentReminderJob).to receive(:perform_later).with(user)
    job
  end

  it 'calls UserMailer with #send_student_reminder_email when performed' do
    # Fails - (SystemStackError: stack level too deep)
    expect(UserMailer).to receive(:send_student_reminder_email)
    perform_enqueued_jobs { job }
  end

  after do
    clear_enqueued_jobs
    clear_performed_jobs
  end
end

The third it block fails with SystemStackError: stack level too deep. Is there a way to only perform the one job in the queue and avoid performing the job from the callback?

Ideally, I'd then like to expand my test coverage to ensure that a new job is enqueued at a given date and time.


Solution

  • I suspect that the issue here is that the perform_enqueued_jobs helper attempts to execute all jobs in the queue until that point, but the queue list is never exhausted, because each job queues another job (hence the loop).

    Try this,

    1. First, put the job in the queue with perform_later.
    2. Get the parent job details from the queue currently_queued_job = enqueued_jobs.first.
    3. Call perform_enqueued_jobs but use the :only option to filter/limit the jobs that will get executed. For instance,
    perform_enqueued_jobs(
      only: ->(job) { job['job_id'] == currently_queued_job['job_id'] }
    )
    

    Here's the Rails documentation for the perform_enqueued_jobs, mentioning:

    :only and :except options accepts Class, Array of Class or Proc. When passed a Proc, an instance of the job will be passed as argument.