Reputation: 889
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
Upvotes: 4
Views: 2903
Reputation: 33
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.
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
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.
Upvotes: 1
Reputation: 23327
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.
Upvotes: 4