skip to Main Content

I’m not sure if I have a bug somewhere, or am just not using good practice. Suppose I have either of the following:

class ThingMailer < ApplicationMailer
  def notify_of_thing
    mail(subject: 'Thing has happened')
  end
end

# ... and elsewhere...

class ThingDoer
  def do_thing
    ThingMailer.notify_of_thing.deliver_later(wait: 30.seconds)
  end
end

or

class ThingWorker
  include Sidekiq::Worker

  def perform(name)
    some_model.update(name: name)
  end
end

# ... and elsewhere...

class ThingPerformer
  def perform_thing
    ThingWorker.perform_in(30.seconds, 'Bob')
  end
end

And elsewhere I have a feature test (or other highish level test) which, in the normal course of things, causes ThingPerformer#perform_thing or ThingDoer#do_thing to be called. What’s the best practice for dealing with them in a test suite?

In my actual test suite, if I don’t just stub out one of the thread-launching methods, and if I’m not running Redis in the background while the tests run, I get the error Error connecting to Redis on 127.0.0.1:6379 (Errno::ECONNREFUSED) (Redis::CannotConnectError).

In config/environments/production.rb, we specify the cache store:

config.cache_store = :redis_store, ENV['REDIS_URL'], { expires_in: 90.minutes }

But our config/environments/test.rb, we specify that the app shouldn’t perform caching (though presumably that’s not working, and maybe just fixing whatever’s causing this issue would be the answer to the first question?)

Here’s the test.rb file:

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # The test environment is used exclusively to run your application's
  # test suite. You never need to work with it otherwise. Remember that
  # your test database is "scratch space" for the test suite and is wiped
  # and recreated between test runs. Don't rely on the data there!
  config.cache_classes = true

  # Do not eager load code on boot. This avoids loading your whole application
  # just for the purpose of running a single test. If you are using a tool that
  # preloads Rails for running tests, you may have to set it to true.
  config.eager_load = false

  # Configure public file server for tests with Cache-Control for performance.
  config.public_file_server.enabled = true
  config.public_file_server.headers = {
    'Cache-Control' => 'public, max-age=3600'
  }

  # Show full error reports and disable caching.
  config.consider_all_requests_local       = true
  config.action_controller.perform_caching = false

  # Raise exceptions instead of rendering exception templates.
  config.action_dispatch.show_exceptions = false

  # Disable request forgery protection in test environment.
  config.action_controller.allow_forgery_protection = false
  config.action_mailer.perform_caching = false

  # Tell Action Mailer not to deliver emails to the real world.
  # The :test delivery method accumulates sent emails in the
  # ActionMailer::Base.deliveries array.
  config.action_mailer.delivery_method = :test
  # config.active_job.queue_adapter = :test

  # Fix the order in which test cases are executed.
  # config.active_support.test_order = :sorted

  # Print deprecation notices to the stderr.
  config.active_support.deprecation = :stderr

  # Raises error for missing translations
  config.action_view.raise_on_missing_translations = true

  config.cache_store = :null_store
end

2

Answers


  1. Sidekiq provides a testing guide.

    If you want to test the behaviour of the job, use Sidekiq::Testing.inline!. If you just want to check the code without testing the job, use Sidekiq::Testing.fake! and check if jobs are enqueued.

    Login or Signup to reply.
  2. We use Resque in our project and our feature/system specs implement the following, which run and pass without redis-server running:

    Scenario

    A user signs up for a new account, and when the form submits we have the background job send a confirmation email to the provided email address.

    RSpec.describe 'User account registration' do
      before do
        allow(BackgroundJob).to receive(:enqueue)
      end
    
      it do
        it_creates_a_new_account
      end
    
      def it_creates_a_new_account
        fill_in 'email', with: '[email protected]'
        fill_in 'password', with: 'password'
        click_button 'Submit'
        page.has_content? 'Please check your inbox to confirm your account registration'
      end
    end
    

    Basically we are stubbing the enqueue method with no return value, so it is extremely important to unit test what the job actually does further down the line.

    Relevant Documentation

    When the user clicks submit it posts the form, and as you’d expect that would bubble down and eventually hit a background job, adding to the queue, that calls a service, and then sends the email.

    class BackgroundJob
      def self.enqueue(klass_name, *args)
        Resque.enqueue(klass_name, *args)
      end
    end
    

    I’ll leave the below examples for reference of the flow.

    Our (super simple example) form class might look like this:

    #
    # When a user signs up for a new account we
    # need to send them a confirmation email
    class NewUserForm
      def initialize(email:, password:)
        @email    = email
        @password = password
      end
    
      def save
        persist
      end
    
      private
    
      def persist
        User.transaction do
          @new_user ||= create_user
          send_confirmation_email
        end
      end
    
      def create_user
        User.create(email: @email, password: @password)
      end
    
      def send_confirmation_email
        BackgroundJob.enqueue(NewUserAccountJob, @new_user.id)
      end
    end
    

    I’d then unit test the form class by simply checking:

    let!(:form) { described_class.new(email: '[email protected]', password: 'password') }
    
    context 'when user creation succeeds' do
      it 'calls the new user account job' do
        expect(BackgroundJob).to receive(:enqueue).with(
          NewUserAccountJob, kind_of?(Integer)
        )
        form.save
      end
    
      it 'creates a user record with the provided details' do
        expect(User).to receive(:create).with(
          email: '[email protected]', password: 'password'
        )
        form.save 
      end
    end
    

    Then the Job class might look something like:

    ##
    # We need to send a confirmation email to a newly
    # created account before we activate the user
    class NewUserAccountJob
      def self.perform(user_id)
        UserMailer.new_account_confirmation(user_id).deliver_now
      end
    end
    

    Which, if I’m being honest with you, I wouldn’t bother testing at all really.


    Then the mailer class would be something in the vein of:

    class UserMailer < ApplicationMailer
      def new_account_confirmation(user_id)
        @facade ||= ::Users::NewAccountMailerFacade.new(user_id)
    
        mail(to: @facade.recipient, subject: @facade.email_subject)
      end
    end
    

    NB: I’m a big fan of the facade pattern as I find it much easier to collate the related services to a certain action, and also easier to test, but you could very easily skip it and just set @user ||= User.find(user_id) and use @user.email as the recipient and a string for the subject.
    Dealer’s choice here.

    If you went down the facade route it might look like:

    module Users
      class NewAccountMailerFacade
        def initialize(user_id)
          @user_id = user_id
        end
    
        def email_recipient
          @email_recipient ||= user.email
        end
    
        def email_subject
          I18n.t('users.new_account_confirmation.subject')
        end
    
        private
    
        def user
          @user ||= User.select(:email).find(@user_id)
        end
      end
    end
    

    I18n.t('users.new_account_confirmation.subject') = ‘Please confirm your email address to complete your account registration for YOURAPPNAME’

    Then it’s just another simple unit test.

    RSpec.describe Users::NewAccountMailerFacade do
      let(:instance) { described_class.new(user.id) }
    
      let!(:user) { create(:user, email: email, password: 'password') }
    
      describe '.email_subject' do
        subject { instance.email_subject }
    
        it { is_expected.to eq(I18n.t('users.new_account_confirmation.subject')) }
      end
    
      describe '.email_recipient' do
        subject { instance.email_recipient }
    
        context 'given an email of [email protected]' do
          let(:email) { '[email protected]' }
    
          it 'returns the correct email' do
            expect(subject).to eq('[email protected]')
          end
        end
    
        context 'given an email of [email protected]' do
          let(:email) { '[email protected]' }
    
          it 'returns the correct email' do
            expect(subject).to eq('[email protected]')
          end
        end
      end
    end
    

    I’ve used factories here as that’s just what I’m used to. But hopefully, this provides some form of help to you.

    I’d also recommend looking into making the transition to system specs if that’s a possibility in your position. Speaking from experience, our team saw a dramatic decrease in flaky specs, and js (mainly ajax) related troubles we experienced back when we wrote features after making the change.
    To play devil’s advocate though, system specs take MUCH longer to run on our CI than our features did (especially with js: true turned on), so it’s not all sunshine and daisies in the system world, unfortunately.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search