RSpec + Sidekiq: NoMethodError: undefined method 'jobs' MyImportJob - ruby-on-rails

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.

Related

Rails specs for simple sidekiq delete worker failling

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.

Test that Delayed Job is called

I am trying to test that a part of my code is running a DelayedJob.
Here's code:
def start_restream
...
puts 'Here'
Delayed::Job.enqueue(Restream::StartAllJob.new(channel.id))
puts 'After'
...
end
#app/jobs/restream/start_all_job.rb
class Restream::StartAllJob < Struct.new(:channel_id)
def perform
puts "Inside"
...
end
end
In my spec_helper.rb I have Delayed::Worker.delay_jobs = false.
The spec:
it 'runs Delayed::Job::Restream::StartAll' do
post :start_restream, :id => channel.id
expect(Restream::StartAllJob).to receive(:new)
end
It prints out
Here
Inside
After
when running, so I know that it is called. But test fails:
Failure/Error: expect(Restream::StartAllJob).to receive(:new)
(Restream::StartAllJob (class)).new(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Or, if I use expect_any_instance_of(Restream::StartAllJob).to receive(:perform) it says
Failure/Error: example.run
Exactly one instance should have received the following message(s) but didn't: perform
What am I doing wrong and how can I test this?
It's just the mistake I made in the order:
expect(Restream::StartAllJob).to receive(:new) should be written before post :start_restream, :id => channel.id

How to test that a method is called using rspec?

In a model spec, I want to test that certain methods are being called correctly.
#models/object.rb
class Object < ActiveRecord::Base
after_validation :do_this
after_save :enqueue_that
def do_this
# does some stuff, the results of which I don't want to test
end
def enqueue_that
MyWorker.perform_later id
end
end
#spec/models/object.rb
describe Object
describe '#do_this' do
it 'is called on save with passing validations' do
object.save
expect(object).to receive(:do_this)
end
end
describe '#enqueue_that' do
it 'is called after save' do
object.save
expect(MyWorker).to receive(:perform_later).once
end
end
end
The tests are failing with the following
Failure/Error: expect(object).to receive(:do_this).once
(#<Object:0x007fd2101c7160>).do_this(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Failure/Error: expect(MyWorker).to receive(:perform_later).once
(MyWorker (class)).perform_later(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Confusingly, these methods appear to be behaving correctly in the dev environment.
Am I using expect().to receive correctly? Or have my tests uncovered a genuine bug?
You just have things in the wrong order...
it 'is called on save with passing validations' do
expect(object).to receive(:do_this)
object.save
end

How to check what is queued in ActiveJob using Rspec

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

RSpec - How to create helper method available to tests that will automatically embed "it" tests

I am new to ruby/rails/rspec etc.
Using rspec 2.13.1, I want to create a module with a method that can be called from my tests resulting to subsequent calls of the "it" method of the RSpec::Core::ExampleGroup.
My module:
require 'spec_helper'
module TestHelper
def invalid_without(symbols)
symbols = symbols.is_a?(Array) ? symbols : [symbols]
symbols.each do |symbol|
it "should not be valid without #{symbol.to_s.humanize}" do
# Gonna nullify the subject's 'symbol' attribute here
# and expect to have error on it
end
end
end
end
The code above was added to:
spec/support/test_helper.rb
and in my spec_helper.rb, in the RSpec.configure block, I added the following:
config.include TestHelper
Now, in a test, I do the following:
describe Foo
context "when invalid" do
invalid_without [:name, :surname]
end
end
Running this, I get:
undefined method `invalid_without' for #<Class:0x007fdaf1821030> (NoMethodError)
Any help appreciated..
Use shared example group.
shared_examples_for "a valid array" do |symbols|
symbols = symbols.is_a?(Array) ? symbols : [symbols]
symbols.each do |symbol|
it "should not be valid without #{symbol.to_s.humanize}" do
# Gonna nullify the subject's 'symbol' attribute here
# and expect to have error on it
end
end
end
describe Foo do
it_should_behave_like "a valid array", [:name, :surname]
end

Resources