RSpec test not passing when I know the method is being called - ruby-on-rails

I have the following class:
class UserRewards
def self.grant(reward_id:, submission_id:, user_id:)
debugger
ActiveRecord::Base.transaction do
puts 'ActiveRecord::Base.transaction'
end
end
end
And the following test:
require 'rails_helper'
RSpec.describe UserRewards do
describe '.grant' do
it 'grants a reward to a user' do
user = build_stubbed(:user)
submission = build_stubbed(:submission)
reward = build_stubbed(:reward)
UserRewards.grant(
reward_id: reward.id,
submission_id: submission.id,
user_id: user.id,
)
expect(ActiveRecord::Base).to receive(:transaction)
end
end
end
I simply want to verify that ActiveRecord::Base has received :transaction, when I run the test, but no matter what I do, or how I try to restructure this test, I always get the following failure:
1) UserRewards.grant grants a reward to a user
Failure/Error: expect(ActiveRecord::Base).to receive(:transaction)
(ActiveRecord::Base (class)).transaction(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
I've verified the method is being run via the debugger:
(rdbg) n # next command
[1, 10] in ~/workspace/my-app/lib/services/user_rewards.rb
1| class UserRewards
2| def self.grant(reward_id:, submission_id:, user_id:)
3| debugger
4| ActiveRecord::Base.transaction do
=> 5| puts 'ActiveRecord::Base.transaction'
6| end
7| end
8|
9| def self.revoke(reward_id:, submission_id:, user_id:)
=>#0 block in grant at ~/workspace/my-app/lib/services/user_rewards.rb:6
#1 block in within_new_transaction at ~/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/activerecord-7.0.1/lib/active_record/connection_adapters/abstract/transaction.rb:319
# and 74 frames (use `bt' command for all frames)
I know I must be doing something wrong here. I don't have to stub the method on ActiveRecord::Base, do I?

If you just need to know that a transaction was called you can stub this method and then check that it was called. Your it is very fat, it's better to use AAA-principle
require 'rails_helper'
describe UserRewards do
describe '.grant' do
let(:user) { build_stubbed(:user) }
let(:submission) { build_stubbed(:submission) }
let(:reward) { build_stubbed(:reward) }
before do
allow(ActiveRecord::Base).to receive(:transaction)
described_class.grant(
reward_id: reward.id,
submission_id: submission.id,
user_id: user.id,
)
end
it 'calls database transaction' do
expect(ActiveRecord::Base).to have_received(:transaction).once
end
end
end

Related

Rails 5 - Sidekiq worker shows job done but nothing happens

I'm using Sidekiq for delayed jobs with sidekiq-status and sidekiq-ent gems. I've created a worker which is reponsible to update minor status to false when user is adult and has minor: true. This worker should be fired every day at midnight ET. Like below:
#initializers/sidekiq.rb
config.periodic do |mgr|
# every day between midnight 0 5 * * *
mgr.register("0 5 * * *", MinorWorker)
end
#app/workers/minor_worker.rb
class MinorWorker
include Sidekiq::Worker
def perform
User.adults.where(minor: true).remove_minor_status
rescue => e
Rails.logger.error("Unable to update minor field. Exception: #{e.message} : #{e.backtrace.join('\n')}")
end
end
#models/user.rb
class User < ApplicationRecord
scope :adults, -> { where('date_of_birth <= ?', 18.years.ago) }
def self.remove_minor_status
update(minor: false)
end
end
No I want to check this on my local machine - to do so I'm using gem 'timecop' to timetravel:
#application.rb
config.time_zone = 'Eastern Time (US & Canada)'
#config/environments/development.rb
config.after_initialize do
t = Time.local(2021, 12, 21, 23, 59, 0)
Timecop.travel(t)
end
After firing up sidekiq by bundle exec sidekiq and bundle exec rails s I'm waiting a minute and I see that worker shows up:
2021-12-21T22:59:00.130Z 25711 TID-ovvzr9828 INFO: Managing 3 periodic jobs
2021-12-21T23:00:00.009Z 25711 TID-ovw69k4ao INFO: Enqueued periodic job SettlementWorker with JID ddab15264f81e0b417e7dd83 for 2021-12-22 00:00:00 +0100
2021-12-21T23:00:00.011Z 25711 TID-ovw69k4ao INFO: Enqueued periodic job MinorWorker with JID 0bcd6b76d6ee4ff9e7850b35 for 2021-12-22 00:00:00 +0100
But it didn't do anything, the user's minor status is still set to minor: true:
2.4.5 :002 > User.last.date_of_birth
=> Mon, 22 Dec 2003
2.4.5 :001 > User.last.minor
=> true
Did I miss something?
EDIT
I have to add that when I'm trying to call this worker on rails c everything works well. I've got even a RSpec test which also passes:
RSpec.describe MinorWorker, type: :worker do
subject(:perform) { described_class.new.perform }
context 'when User has minor status' do
let(:user1) { create(:user, minor: true) }
it 'removes minor status' do
expect { perform }.to change { user1.reload.minor }.from(true).to(false)
end
context 'when user is adult' do
let(:registrant2) { create(:registrant) }
it 'not change minor status' do
expect(registrant2.reload.minor).to eq(false)
end
end
end
end
Since this is the class method update won't work
def self.remove_minor_status
update(minor: false)
end
Make use of #update_all
def self.remove_minor_status
update_all(minor: false)
end
Also, I think it's best practice to have some test cases to ensure the working of the methods.
As of now you can try this method from rails console and verify if they actually work
test "update minor status" do
user = User.create(date_of_birth: 19.years.ago, minor: true)
User.adults.where(minor: true).remove_minor_status
assert_equal user.reload.minor, false
end
I think you need to either do update_all or update each record by itself, like this:
User.adults.where(minor: true).update_all(minor: false)
or
class MinorWorker
include Sidekiq::Worker
def perform
users = User.adults.where(minor: true)
users.each { |user| user.remove_minor_status }
rescue => e
Rails.logger.error("Unable to update minor field. Exception: #{e.message} : #{e.backtrace.join('\n')}")
end
end
You may also want to consider changing update to update! so it throws an error if failing to be caught by your rescue in the job:
def self.remove_minor_status
update!(minor: false)
end

Rails Rspec allow multiple method call in one line

desc 'Remove credential state users who no longer request for confirm otp within 10 minutes'
task failed_user_cleaner: :environment do
puts "Daily UserRecord Cleaning CronJob started - #{Time.now}"
#user = User.with_state("credentials").with_last_otp_at(Time.now - 10.minutes)
Users::Delete.new(#user).destroy_all
puts "Daily UserRecord Cleaning CronJob ended - #{Time.now}"
end
Above is crop job rake file code.
then I've tried in many times and found in many times.
But I couldn't find the way to write unit test case for above job.
Help me to write test case correctly.
here is my spec code
require 'rails_helper'
describe 'users rake tasks' do
before do
Rake.application.rake_require 'tasks/users'
Rake::Task.define_task(:environment)
end
context 'when remove credential state users who no longer request for confirm otp within 10 minutes' do
let(:user) { create(:user, last_otp_at: Time.now - 11.minutes, state: "credentials") }
let (:run_users_rake_task) do
Rake.application.invoke_task 'users:failed_user_cleaner'
end
it 'calls right service method' do
#users = Users::Delete.new([user])
expect(#users).to receive(:destroy_all)
run_users_rake_task
end
end
end
here is the error log
Failures:
1) users rake tasks when remove credential state users who no longer request for confirm otp within 10 minutes calls right service method
Failure/Error: expect(#users).to receive(:destroy_all)
(#<Users::Delete:0x0000556dfcca3a40 #user=[#<User id: 181, uuid: nil, phone: "+66969597538", otp_secret: nil, last_otp_at: "2021-09-30 09:32:24.961548000 +0700", created_at: "2021-09-30 09:43:24.973818000 +0700", updated_at: "2021-09-30 09:43:24.973818000 +0700", email: nil, avatar: "https://dummyimage.com/300x300/f04720/153572.png?t...", refresh_token: "eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MzI5Njk4MDQsImV4c...", first_name_en: "Jenise", first_name_th: "Damion", last_name_en: "McCullough", last_name_th: "Beatty", nationality: "TH", thai_national_id: nil, thai_laser_code: nil, company_id: 200, role: nil, state: "credentials", date_of_birth: "2020-10-30 00:00:00.000000000 +0700", deleted_at: nil, password_digest: "$2a$04$jfR9X9ci06602tlAyLOoRewTK1lZ12vJ2cZ9Dc2ov4F...", username: "zreejme238", shopname: nil, access_token: nil, locked_at: nil, login_attempts: 0, locale: "th", scorm_completed: false>]>).destroy_all(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
# ./spec/tasks/users_spec.rb:19:in `block (3 levels) in <top (required)>'
You are creating two instances of Users::Delete when running this test, one within the test and one within the task. Since the instance within the test is not used, it is incorrect to expect it to receive a message.
Rspec has an expectation, expect_any_instance_of, that will fix this however consider reading the full page since it can create fragile or flaky tests. If you wanted to use this method, your test would look something like:
it 'calls right service method' do
expect_any_instance_of(Users::Delete).to receive(:destroy_all)
run_users_rake_task
end
Personally I'd instead check that the expected users were deleted with something like:
it 'removes the user' do
expect { run_users_rake_task }.to change { User.exists?(id: #user.id) }.to(false)
end
Unless you want to use any_instance_of (which is a code smell) you need to stub the Users::Delete method so that it returns a double and put the expectation on the double:
require 'rails_helper'
describe 'users rake tasks' do
before do
Rake.application.rake_require 'tasks/users'
Rake::Task.define_task(:environment)
end
context 'when remove credential state users who no longer request for confirm otp within 10 minutes' do
let(:user) { create(:user, last_otp_at: Time.now - 11.minutes, state: "credentials") }
let(:run_users_rake_task) do
Rake.application.invoke_task 'users:failed_user_cleaner'
end
let(:double) do
instance_double('Users::Delete')
end
before do
allow(Users::Delete).to receive(:new).and_return(double)
end
it 'calls right service method' do
expect(double).to receive(:destroy_all)
run_users_rake_task
end
end
end
However this really just tells us that the API of the service object is clunky and that you should write a class method which both instanciates and performs:
module Users
class Delete
# ...
def self.destroy_all(users)
new(users).destroy_all
end
end
end
desc 'Remove credential state users who no longer request for confirm otp within 10 minutes'
#...
Users::Delete.destroy_all(#user)
# ...
end
require 'rails_helper'
describe 'users rake tasks' do
# ...
context 'when remove credential state users who no longer request for confirm otp within 10 minutes' do
# ...
it 'calls right service method' do
expect(Users::Delete).to receive(:destroy_all)
run_users_rake_task
end
end
end

Rspec: test if method was called on an object inside a job?

I made a job to call a method if ended_at was less than today. For some reason I keep getting failures even if I throw a binding pry right at the moment it is about to break and manually call the method and it works fine. However, I still get a failure if I just let the spec test run on its own.
My job:
class RemoveOldEventsFromAlgoliaJob < ApplicationJob
queue_as :default
def perform
old_events = Event.where("ended_at < ?", Date.today)
old_events.each do |e|
e.remove_from_algolia
end
end
end
My method:
def remove_from_algolia
index = Algolia::Index.new(##ALGOLIA_INDEX_NAME)
index.delete_object(self.id)
end
My spec test:
require 'rails_helper'
RSpec.describe RemoveOldEventsFromAlgoliaJob, type: :job do
it "will remove old events from the index" do
ActiveJob::Base.queue_adapter = :test
event = FactoryGirl.create(:event, title: "EXPIRED EVENT", ended_at: 1.day.ago)
RemoveOldEventsFromAlgoliaJob.perform_now
expect(event).to receive(:remove_from_algolia)
end
end
RSpec output:
Failures:
1) d will remove old events from the index
Failure/Error: expect(event).to receive(:remove_from_algolia)
(#<Event id: 401, uuid: "6e9a6f08-c34d-45af-9f3c-870b28643809", organization_id: nil, event_type_id: nil, name: "cool-event", title: "Cool Event", description: "Rad thing that's gonna happen", platform_type: "ee", platform_id: "12345678", platform_url: "http://event.com/12345678", featured: false, capacity: 100, rsvp_count: nil, attendee_count: nil, status: "upcoming", started_at: "2017-10-21 03:00:39", ended_at: "2017-03-23 07:00:00", deleted_at: "2017-10-21 03:00:39", created_at: "2017-03-24 00:24:54", updated_at: "2017-03-24 00:24:54", location_line_1: nil, location_line_2: nil, location_city: nil, location_state: nil, location_zip: nil, location_country: nil>).remove_from_algolia(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
# ./spec/jobs/remove_old_events_from_algolia_job_spec.rb:10:in `block (2 levels) in <top (required)>'
Finished in 0.8828 seconds (files took 7.12 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/jobs/remove_old_events_from_algolia_job_spec.rb:4 # RemoveOldEventsFromAlgoliaJob will remove old events from the index
Coverage report generated for RSpec to /workproject/coverage. 384 / 496 LOC (77.42%) covered.
I can put a binding.pry inside my job during the spec test and call that method successfully:
4: def perform
5:
6: old_events = Event.where("ended_at < ?", Date.today)
7: old_events.each do |e|
=> 8: binding.pry
9: e.remove_from_algolia
10: end
11: end
[1] pry(#<RemoveOldEventsFromAlgoliaJob>)> e.remove_from_algolia
=> {"deletedAt"=>"2017-03-24T00:37:53.743Z", "taskID"=>206043962, "objectID"=>"409"}
[2] pry(#<RemoveOldEventsFromAlgoliaJob>)>
Try using a stub for event instead of a real Event model:
require 'rails_helper'
RSpec.describe RemoveOldEventsFromAlgoliaJob, type: :job do
it "will remove old events from the index" do
ActiveJob::Base.queue_adapter = :test
event = double('event')
allow(Event).to receive(:where).and_return([event])
expect(event).to receive(:remove_from_algolia)
RemoveOldEventsFromAlgoliaJob.perform_now
end
end

RSpec allow_any_instance_of does not work within request test

I'm new to RSpec, and testing out some webhook test with request type.
But here even I use allow_any_instance_of, it errors out got 500 instead of 200. I checked every variable with binding.pry but it seems all okay.
In my opinion, the mocking fails so it returns 500.
Any ideas?
describe "stripe_invoice_created_webhook", type: :request do
let(:card_invoice){ create(:card_invoice, id: invoice.id) }
let(:invoice){ create(:invoice, payment_account_id: payment_card_account.payment_account_id) }
let(:payment_card_account){ create(:payment_card_account,
stripe_customer_id: event.data.object.customer) }
let(:event){ StripeMock.mock_webhook_event('invoice.created', {
closed: false
}) }
it 'responds 200 to invoice_created webhook with valid endpoint' do
allow_any_instance_of(CardInvoice).to receive(:process_invoice_items)
allow_any_instance_of(CardInvoice).to receive(:process!)
post '/stripe-events', event.as_json
expect(response.status).to eq 200
expect{ card_invoice.process_invoice_items }.not_to raise_error
expect{ card_invoice.process! }.not_to raise_error
end
and the original code is
class InvoiceCreated
def call(event)
invoice = event.data.object
# NOTE: Skip if the invoice is closed.
if invoice.closed == false
stripe_customer = invoice.customer
payment_account = PaymentCardAccount.find_by(stripe_customer_id: stripe_customer)
card_invoice = Invoice.find_card_invoice_in_this_month_within(payment_account: payment_account)
card_invoice.process_invoice_items(stripe_customer: stripe_customer,
event_invoice_id: invoice.id)
card_invoice.process!(:pending, id: invoice.id)
end
end
end
Yeah the mocking fails. You are expecting the object CardVoice to receive process! or process_invoice_item but you have notnta specified a return value. The syntax for allow_any_instance_of is
allow_any_instance_of(Object).to receive(:function).and_return(:return_value)

Tests are not passing

I'm using RSpec for tests and I don't know how to get this to green.
In this case, I have a model called "PartType" that holds an attribute called "quotation".
The value for quotation comes from a form, so it will be a string.
To demonstrate you can go to console and type:
(1..1000).includes?("50") # false
but..
(1..1000).includes?(50) # true
And this value can have decimals. So I needed to do a "type_cast".
I have this on my PartTypemodel:
before_validation :fix_quotation, :if => :quotation_changed?
protected
def fix_quotation
self[:quotation] = quotation_before_type_cast.tr(' $, ' , '.' )
end
This are working as expected BUT when go to tests, it fails.
Here is my part_type_spec.rb:
require 'spec_helper'
describe PartType do
before(:each) do
#attr = { :title => "Silver", :quotation => 100 }
end
it "should create a instance given a valid attributes" do
PartType.create!(#attr)
end
it "should accept null value for quotation" do
PartType.new(#attr.merge(:quotation => nil)).should be_valid
end
it "should accept 0 value for quotation" do
PartType.new(#attr.merge(:quotation => 0)).should be_valid
end
end
And finally the failing tests:
Failures:
1) PartType should create a instance given a valid attributes
Failure/Error: PartType.create!(#attr)
NoMethodError:
undefined method tr' for 100:Fixnum
# ./app/models/part_type.rb:7:infix_quotation'
# ./spec/models/part_type_spec.rb:10:in `block (2 levels) in '
2) PartType should accept 0 value for quotation
Failure/Error: PartType.new(#attr.merge(:quotation => 0)).should be_valid
NoMethodError:
undefined method tr' for 0:Fixnum
# ./app/models/part_type.rb:7:infix_quotation'
# ./spec/models/part_type_spec.rb:18:in `block (2 levels) in '
Finished in 0.06089 seconds
3 examples, 2 failures
Your include? snippets are wrong, I got false in the first, true in the second.
before_validation is executed and quotation_before_type_cast is expected to be a String but it is a Fixnum. Change 100 to '100' and 0 to '0'.

Resources