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.
Related
I have a mailer that passes an argument like so:
AnimalMailer.daily_message(owner).deliver_later
The method looks like this:
AnimalMailer
class AnimalMailer < ApplicationMailer
def daily_message(owner)
mail(
to: "#{user.name}",
subject: "test",
content_type: "text/html",
date: Time.now.in_time_zone("Mountain Time (US & Canada)")
)
end
end
I'm new to writing specs and was wondering how should I pass the owner to the method and test it. I currently have this set up:
require "rails_helper"
RSpec.describe AnimalMailer, type: :mailer do
describe "monthly_animal_message" do
let(:user) { create(:user, :admin) }
it "renders the headers" do
expect(mail.subject).to eq("test")
expect(mail.to).to eq(user.name)
end
end
end
Specs generally follow a three-step flow 1) set up, 2) invoke, 3) expect. This applies for unit testing mailers like anything else. The invocation and parameters are the same in the test as for general use, so in your case:
RSpec.describe AnimalMailer, type: :mailer do
describe "monthly_campaign_report" do
let(:user) { create(:user, :admin) }
let(:mail) { described_class.daily_message(user) } # invocation
it 'renders the headers' do
expect(mail.subject).to eq('test')
expect(mail.to).to eq(user.name)
end
it 'renders the body' do
# whatever
end
end
end
Note that since the describe is the class name being tested, you can use described_class from there to refer back to the described class. You can always use AnimalMailer.daily_message as well, but among other things described_class ensures that if you shuffle or share examples that you are always testing what you think you are.
Also note that in the case of unit testing a mailer, you're mostly focused on the correct generation of the content. Testing of successful delivery or use in jobs, controllers, etc., would be done as part of request or feature tests.
Before testing it, make sure the config / environment / test.rb file is set to:
config.action_mailer.delivery_method = :test
This ensures that emails are not actually sent, but are stored in the ActionMailer :: Base.deliveries array.
Following Four-Phase Test :
animal_mailer.rb
class AnimalMailer < ApplicationMailer
default from: 'noreply#animal_mailer.com'
def daily_message(owner)
#name = owner.name
mail(
to: owner.email,
subject: "test",
content_type: "text/html",
date: Time.now.in_time_zone("Mountain Time (US & Canada)")
)
end
end
animal_mailer_spec.rb
RSpec.describe AnimalMailer, type: :mailer do
describe 'instructions' do
let(:user) { create(:user, :admin) }
let(:mail) { described_class.daily_message(user).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("test")
end
it 'renders the receiver email' do
expect(mail.to).to eq([user.email])
end
it 'renders the sender email' do
expect(mail.from).to eq(['noreply#animal_mailer.com'])
end
it 'assigns #name' do
expect(mail.body.encoded).to match(user.name)
end
end
end
if you have a model user:
class User
def send_instructions
AnimalMailer.instructions(self).deliver_now
end
end
RSpec.describe User, type: :model do
subject { create :user }
it 'sends an email' do
expect { subject.send_instructions }
.to change { ActionMailer::Base.deliveries.count }.by(1)
end
end
I can't understand, how to test this case. I googled it but didn't find anything.
As a result (i think i am correct) I should expect to return 2 objects with new statuses false because I pass in subject.call id by first object what created in let.
Please someone can help me and explain to me how to test update_all and other update cases. Thanks!
#rspec
describe Plans::MakeAllPlansInactive do
subject do
described_class.call(plan_id: plan.id)
end
let!(:plan) do
create(:plan, active: true)
end
let!(:plan_1) do
create(:plan, active: true)
end
let!(:plan_2) do
create(:plan, active: true)
end
context 'when success' do
it 'makes one active, other passive' do
subject.to eq(2)
end
end
#service
def call
return unless Plan.find(plan_id).active?
update_our_plans
end
private
def update_our_plans
Plan.where.not(id: plan_id).update_all(active: false)
end
For this service you obviously are more interested in the side-effect, than return value, so in spec describe what state you are expecting with a test like:
it 'makes one active, other passive' do
expect(Plan.count).to eq(3) # just to be sure
expect{ subject }.to change{ plan_1.reload.active }.from(true).to(false).and(
change{ plan_2.reload.active }.from(true).to(false)
).and(not_change{ plan.reload.active })
end
I can't find a way around this.
This is my test:
require 'rails_helper'
RSpec.describe V1::UsersController do
describe '#create' do
let(:post_params) do
{
first_nm: Faker::Name.first_name,
last_nm: Faker::Name.last_name ,
password: "test123456",
password_confirmation: "test123456",
email_address: Faker::Internet.email
}
end
before do
post :create, params: post_params
end
context 'successful create' do
subject(:user) { User.find_by_email(post_params[:email_address]) }
it 'persists the user' do
expect(user).not_to be_nil
end
it 'user data is correct' do
post_params.except(:password, :password_confirmation).each do |k, v|
expect(user.send(k)).to eq(v)
end
end
it 'returns responsde code of 201' do
expect(response.status).to eq(201)
end
end
end
end
I only want this controller to be hit once. However, I can't seem to get that to work.
I have tried setting before(:context) and I get an error
RuntimeError:
let declaration `post_params` accessed in a `before(:context)` hook at:
`let` and `subject` declarations are not intended to be called
in a `before(:context)` hook, as they exist to define state that
is reset between each example, while `before(:context)` exists to
define state that is shared across examples in an example group.
I don't want multiple users to be persisted for such a simple test. I also dont want to be hitting the api for every example.
I want the before block to run once. How can I do this?
As the error message states, let and subject are specifically for managing per-example state. But before(:context)/before(:all) hooks get run outside the scope of any specific example, so they are fundamentally incompatible. If you want to use before(:context), you can't reference any let definitions from the hook. You'll have to manage the post_params state yourself without using let. Here's a simple way to do that:
require 'rails_helper'
RSpec.describe V1::UsersController do
describe '#create' do
before(:context) do
#post_params = {
first_nm: Faker::Name.first_name,
last_nm: Faker::Name.last_name ,
password: "test123456",
password_confirmation: "test123456",
email_address: Faker::Internet.email
}
post :create, params: #post_params
end
context 'successful create' do
subject(:user) { User.find_by_email(#post_params[:email_address]) }
it 'persists the user' do
expect(user).not_to be_nil
end
it 'user data is correct' do
#post_params.except(:password, :password_confirmation).each do |k, v|
expect(user.send(k)).to eq(v)
end
end
it 'returns responsde code of 201' do
expect(response.status).to eq(201)
end
end
end
end
That should solve your problem; however it's not the approach I would recommend. Instead, I recommend you use the aggregate_failures feature of RSpec 3.3+ and put all of this in a single example, like so:
require 'rails_helper'
RSpec.describe V1::UsersController do
describe '#create' do
let(:post_params) do
{
first_nm: Faker::Name.first_name,
last_nm: Faker::Name.last_name ,
password: "test123456",
password_confirmation: "test123456",
email_address: Faker::Internet.email
}
end
it 'successfully creates a user with the requested params', :aggregate_failures do
post :create, params: post_params
expect(response.status).to eq(201)
user = User.find_by_email(post_params[:email_address])
expect(user).not_to be_nil
post_params.except(:password, :password_confirmation).each do |k, v|
expect(user.send(k)).to eq(v)
end
end
end
end
aggregate_failures gives you a failure report indicating each expectation that failed (rather than just the first one like normal), just like if you had separated it into 3 separate examples, while allowing you to actually make it a single example. This allows you to incapsulate the action you are testing in a single example, allowing you to only perform the action once like you want. In a lot of ways, this fits better with the per-example state sandboxing provided by RSpec's features like before hooks, let declarations and the DB-transaction rollback provided by rspec-rails, anyway. And
I like the aggregate_failures feature so much that I tend to configure RSpec to automatically apply it to every example in spec_helper.rb:
RSpec.configure do |c|
c.define_derived_metadata do |meta|
meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures)
end
end
What you are looking for is before(:all), which will run once before all the cases.
There is similar after(:all) as well.
Interestingly, the before is basically a shorter way of saying before(:each) (which IMO makes more sense).
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.
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