I'm working on a reset_password method in a Rails API app. When this endpoint is hit, an ActiveJob is queued that will fire off a request to Mandrill (our transactional email client). I'm currently trying to write the tests to ensure that that the ActiveJob is queued correctly when the controller endpoint is hit.
def reset_password
#user = User.find_by(email: params[:user][:email])
#user.send_reset_password_instructions
end
The send_reset_password_instructions creates some url's etc before creating the ActiveJob which's code is below:
class SendEmailJob < ActiveJob::Base
queue_as :default
def perform(message)
mandrill = Mandrill::API.new
mandrill.messages.send_template "reset-password", [], message
rescue Mandrill::Error => e
puts "A mandrill error occurred: #{e.class} - #{e.message}"
raise
end
end
At the moment we are not using any adapters for the ActiveJob, so I just want to check with Rspec that the ActiveJob is queued.
Currently my test looks something like this (I'm using factory girl to create the user):
require 'active_job/test_helper'
describe '#reset_password' do
let(:user) { create :user }
it 'should create an ActiveJob to send the reset password email' do
expect(enqueued_jobs.size).to eq 0
post :reset_password, user: { email: user.email }
expect(enqueued_jobs.size).to eq 1
end
end
Everything works in reality, I just need to create the tests!
I'm using ruby 2.1.2 and rails 4.1.6.
I can't see any documentation or help anywhere on the web on how to test on this so any help would be greatly appreciated!
The accepted answer no longer works for me, so I tried Michael H.'s suggestion in the comments, which works.
describe 'whatever' do
include ActiveJob::TestHelper
after do
clear_enqueued_jobs
end
it 'should email' do
expect(enqueued_jobs.size).to eq(1)
end
end
In a unit test, instead of checking what is queued one can also rely on ActiveJob working properly and just verify that it will be called by mocking its api.
expect(MyJob).to receive(:perform_later).once
post :reset_password, user: { email: user.email }
The creators of the ActiveJob have used the same techniques for their unit tests. See GridJob Testobject
They create a testmock GridJob in their tests and override the perform method, so that it only adds jobs to a custom Array, they call JobBuffer. At the end they test, whether the buffer has jobs enqueued
At a single place one can ofc also do an integrations test. The ActiveJob test_helper.rb is supposed to be used with minitest not with rspec. So you have to rebuild it's functionalitity. You can just call
expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to eq 1
without requiring anything
Update 1:
As noticed within a comment.
ActiveJob::Base.queue_adapter.enqueued_jobs works only by setting it the queue_adapter into test mode.
# either within config/environment/test.rb
config.active_job.queue_adapter = :test
# or within a test setup
ActiveJob::Base.queue_adapter = :test
Rspec 3.4 now has have_enqueued_job cooked in, which makes this a lot easier to test:
it "enqueues a YourJob" do
expect {
get :your_action, {}
}.to have_enqueued_job(YourJob)
end
it has other niceties for have_enqueued_job to allow you to check the argument(s) and the number of times it should be queued up.
Testing Rails ActiveJob with RSpec
class MyJob < ActiveJob::Base
queue_as :urgent
rescue_from(NoResultsError) do
retry_job wait: 5.minutes, queue: :default
end
def perform(*args)
MyService.call(*args)
end
end
require 'rails_helper'
RSpec.describe MyJob, type: :job do
include ActiveJob::TestHelper
subject(:job) { described_class.perform_later(123) }
it 'queues the job' do
expect { job }
.to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(1)
end
it 'is in urgent queue' do
expect(MyJob.new.queue_name).to eq('urgent')
end
it 'executes perform' do
expect(MyService).to receive(:call).with(123)
perform_enqueued_jobs { job }
end
it 'handles no results error' do
allow(MyService).to receive(:call).and_raise(NoResultsError)
perform_enqueued_jobs do
expect_any_instance_of(MyJob)
.to receive(:retry_job).with(wait: 10.minutes, queue: :default)
job
end
end
after do
clear_enqueued_jobs
clear_performed_jobs
end
end
There is a new rspec extension which makes your life easier.
require 'rails_helper'
RSpec.describe MyController do
let(:user) { FactoryGirl.create(:user) }
let(:params) { { user_id: user.id } }
subject(:make_request) { described_class.make_request(params) }
it { expect { make_request }.to enqueue_a(RequestMaker).with(global_id(user)) }
end
In my opinion, ensure a job was enqueued when a request is performed is important.
You can do it with the below solutions:
Solution 1
expect{ post your_api_here, params: params, headers: headers }
.to have_enqueued_job(YourJob)
.with(args)
Solution 2
expect(YourJob).to receive(:perform_later).once.with(args)
post your_api_here, params: params, headers: headers
I had some problems, maybe because I didn't include ActiveJob::TestHelper, but this worked for me...
Firstly ensure, that you have the queue adapter set to :test as above answers show.
For some reason clear_enqueued_jobs jobs in the after block didn't work for me, but the source shows we can do the following: enqueued_jobs.clear
require 'rails_helper'
include RSpec::Rails::Matchers
RSpec.describe "my_rake_task", type: :rake do
after do
ActiveJob::Base.queue_adapter.enqueued_jobs.clear
end
context "when #all task is run" do
it "enqueues jobs which have been enabled" do
enabled_count = get_enabled_count
subject.execute
expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq(enabled_count)
end
it "doesn't enqueues jobs which have been disabled" do
enabled_count = get_enabled_count
subject.execute
expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq(enabled_count)
end
end
end
I think the solutions using expect { your_code }.to have_enqueued_job(YourJob) to be very clean, since they use the "official" assertions. If you do not like long blocks passed to expect, you can also use:
YourJob.perform_later
expect(YourJob).to have_been_enqueued
Please find good examples in the rubydoc documentation.
A simple solution is
# frozen_string_literal: true
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked
# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
#
def self.my_jobs
enqueued_jobs.select{|x| x['job_class'] == self.name}
end
end
then you can use helper method my_jobs in test like
require 'rails_helper'
RSpec.describe SendBookingRemindersJob, type: :job do
describe '.start_time_approaching' do
let!(:booking) { create :booking }
it 'schedules 4 jobs' do
SendBookingRemindersJob.start_time_approaching(booking)
expect(SendBookingRemindersJob.my_jobs.count).to eq(4)
end
end
Related
here a small description of my code (simplified)
app/jobs/
class GenerateInvoiceJob < ActiveJob::Base
queue_as :default
def perform()
Invoice.create
end
end
app/models/
class Product < ActiveRecord::Base
def buy
GenerateInvoiceJob.perform_later
end
end
spec/jobs
RSpec.describe AnotherJob, type: :job do
context "with filter" do
...
end
end
spec/models
RSpec.describe Product, type: :model do
describe '#buy' do
it "should generate invoice" do
Product.create().buy
expect(Invoice.all.size).to eq 1
end
end
end
with rails 4.2.11
when I run
rspec spec/models/product_spec.rb
then the test is ok (the job is performed)
when I run
rspec spec -e 'should generate invoice'
then the test fail cause the job is not performed
if I delete all test jobs from spec/jobs and then run
rspec spec -e 'should generate invoice'
then the test is ok (the job is performed)
I can't understand why having some tests for jobs prevents other jobs to perform ? Is there a solution for this?
with rails 5 and rails 6
whatever I do, the test always failed as the job is never performed ?
Aren't jobs performed anymore during tests since rails 5 ?
thanks for help
update 1 after first answer :
thanks a lot for your answer
just to be sure I do correctly :
I added in environment/test.rb
config.active_job.queue_adapter = :test
and in my spec/models/product_spec.rb
RSpec.describe Product, type: :model do
describe '#buy' do
it "should generate invoice" do
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
Product.create().buy
expect(Invoice.all.size).to eq 1
end
end
end
not sure I put
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
at the good place ?!
You need to set:
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
However, using have_enqueued_job is a more common approach.
EDIT: There's even an easier way that slipped my mind:
ActiveJob::Base.queue_adapter = :inline
you need to wrap your code in perform_enqueued_jobs
it 'test request' do
perform_enqueued_jobs do
expect(request).to be_success
end
end
I've got simple sidekiq worker which, I don't know why it doesn't worked. I think maybe it's because of specs.
worker
class AdminPanelLogRemoverWorker
include Sidekiq::Worker
def perform
expired_logs = AdminPanelLog.where('created_at > ?', 1.year.ago)
expired_logs.delete_all
end
end
specs
require 'rails_helper'
describe AdminPanelLogRemoverWorker do
include_context 'with admin_user form'
subject { described_class.new.perform }
let!(:admin_panel_log1) do
create :admin_panel_log,
action_type: 'Update',
old_data: admin_user_form,
created_at: 2.years.ago
end
let!(:admin_panel_log2) do
create :admin_panel_log,
old_data: admin_user_form,
created_at: 2.days.ago
end
context 'when admin log is outdated' do
it 'calls remover worker' do
expect(AdminPanelLog.count).to eq(1)
end
end
end
The admin_panel_log1 and admin_panel_log2 is corresponding model AdminPanelLog and it forms correctly (maybe I should avoid let! ?). At the result specs failed with an error
Failure/Error: expect(AdminPanelLog.count).to eq(1)
expected: 1
got: 0
(compared using ==)
I justed tested with
RSpec.describe TestController, type: :controller do
subject { User.new }
let!(:test) do
p subject
p "dfvb"
end
it 'testing order of let and subject' do
# Spec
end
end
The subject is initialized before the let! block is called. So in your case, the lo AdminPanelLog is not even created while the job was running. So that the example failed.
context 'when the admin log is outdated' do
it 'calls remover worker' do
subject.new.perform #Perform the job here or after the initialization of AdminPanelLog
expect(AdminPanelLog.count).to eq(1)
end
end
and remove this subject { described_class.new.perform }, as the subject itself will hold the value of the current class.
As already transpires from the Aarthi answer, the issue was that you did not call subject, so the code was not executed and your worker was not called.
Still, I would improve the answer with the following
context 'when admin log is outdated' do
it 'remover worker deletes them' do
expect { subject }.to change(AdminPanelLog, :count).by(-2) #or whatever the amount is
end
end
The above test allows you to check if the worker indeed did it's job at deleting stuff.
I'm doing a test using RSPEC, and used Sidekiq for background jobs.
Since, there's no generator for workers in rspec, not sure what to use.
https://relishapp.com/rspec/rspec-rails/docs/generators
require 'spec_helper'
RSpec.describe TestWorker, type: ? do # ex. :worker :sidekiq ...
describe "TestWorker" do
it "" do
....
end
end
end
bundle exec rspec spec/workers/test_worker_spec.rb
Doing like below, i'm getting: uninitialized constant TestWorker
require 'spec_helper'
describe TestWorker do
it "" do
....
end
end
As i tried, gem rspec-sidekiq
https://github.com/philostler/rspec-sidekiq
Can someone provide a sample template for testing app/workers/ in Rspec.
Thanks.
I have not used the rspec-sidekiq gem, however, here is an example of how I am checking for Background jobs which uses sidekiq
# app/spec/workers/demo_worker_spec.rb
require 'rails_helper'
require 'sidekiq/testing'
Sidekiq::Testing.fake!
RSpec.describe DemoWorker, type: :worker do
describe "Sidekiq Worker" do
let (:demo) { FactoryGirl.create(:demo) }
it "should respond to #perform" do
expect(DemoWorker.new).to respond_to(:perform)
end
describe "Demo" do
before do
Sidekiq::Extensions.enable_delay!
Sidekiq::Worker.clear_all
end
it "should enqueue a Email and SMS job" do
assert_equal 0, Sidekiq::Extensions::DelayedMailer.jobs.size
Mailer.delay.demo_request(demo.id)
assert_equal 1, Sidekiq::Extensions::DelayedMailer.jobs.size
end
end
end
end
As of I'm checking, if the instance responds to perform.
Then, I'm asserting before and after the job is scheduled.
You might sometimes want to test more than just the fact that the worker has been enqueud or not.
While it is better to decouple complex stuff that could happen in a worker's perform block, it can be tested as a standard class :
it { expect { MyWorker.new.perform }.to change { ...expectations... } }
or
it do
MyWorker.new.perform
... expectations ..
end
I am trying to write some specs for RSpec + Sidekiq in a Rails 4.2.4 app, but am encountering some issues.
My code looks like this:
class MyImportJob
include Sidekiq::Worker
sidekiq_options queue: :default
def perform(params)
# Do magic
end
end
and the spec:
describe MyImportJob, type: :job do
let(:panel) { create(:panel) }
describe '#perform' do
context 'unsuccessfully' do
it 'raises ArgumentError if no panel param was passed' do
expect {subject.perform_async()}.to raise_error(ArgumentError)
end
end
context 'successfully' do
it 'given a panel, it increases the job number' do
expect {
subject.perform_async(panel_id: panel.id)
}.to change(subject.jobs, :size).by(1)
end
end
end
end
But I am receiving the following errors:
Failure/Error: }.to change(subject.jobs, :size).by(1)
NoMethodError:
undefined method `jobs' for #<MyImportJob:0x007f80b74c5c18>
and
Failure/Error: expect {subject.perform_async()}.to raise_error(ArgumentError)
expected ArgumentError, got #<NoMethodError: undefined method `perform_async' for #<MyImportJob:0x007f80b6d73f50>>
I believe perform_async should be provided by default by Sidekiq as long as I include the line include Sidekiq::Worker in my worker, is this correct? The first test passes if I just use perform but I'd expect it to pass with perform_async which is what I'm using in my codebase.
As for the second, I don't understand why there is no method jobs for the test subject. Any clue about that?
My rails_helper.rb file has:
require 'sidekiq/testing'
Sidekiq::Testing.fake!
Thanks in advance!
In case you don't define subject explicitly, rspec will create subject as following rule:
By default, if the first argument to an outermost example group
(describe or context block) is a class, RSpec creates an instance of
that class and assigns it to the subject
ref: What's the difference between RSpec's subject and let? When should they be used or not?
That means it create instance of your worker. So that you can't call perform_async and jobs.
To resolve your issue, define it explicitly as below:
describe MyImportJob, type: :job do
let(:panel) { create(:panel) }
subject { MyImportJob }
describe '#perform' do
context 'unsuccessfully' do
it 'raises ArgumentError if no panel param was passed' do
expect {subject.perform_async()}.to raise_error(ArgumentError)
end
end
context 'successfully' do
it 'given a panel, it increases the job number' do
expect {
subject.perform_async(panel_id: panel.id)
}.to change(subject.jobs, :size).by(1)
end
end
end
end
expected ArgumentError, got #<NoMethodError: undefined method 'perform_async' for #<MyImportJob:0x007f80b6d73f50>>
perform_async is a method on worker class itself.
MyImportJob.perform_async(...)
I don't understand why there is no method jobs for the test subject
The same exact reason. It's a method on the worker class.
The rspec-rails gem has some support for testing ActiveJob.
Does it provide a way to assert that zero jobs were enqueued?
Rails provides a method called assert_no_enqueued_jobs but it's a little awkward to use in RSpec, because you have to include ::ActiveJob::TestHelper.
RSpec.describe BananaController, type: :controller do
include ::ActiveJob::TestHelper
describe "#create" do
context "access denied" do
it "does not enqueue any jobs" do
# ...
assert_no_enqueued_jobs
end
end
end
end
Is there a better way?
You can try:
expect(ActiveJob::Base.queue_adapter.enqueued_jobs.size).to eq(0)