Rails Rspec allow multiple method call in one line - ruby-on-rails

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

Related

Ruby on Rails - Testing Mailer as a callback in Model

I have this logic where I send a welcome email to the user when it is created.
User.rb
class User < ApplicationRecord
# Callbacks
after_create :send_registration_email
private
def send_registration_email
UserConfirmationMailer.with(user: self).user_registration_email.deliver_later
end
end
I tried testing it in user_spec.rb:
describe "#save" do
subject { create :valid_user }
context "when user is created" do
it "receives welcome email" do
mail = double('Mail')
expect(UserConfirmationMailer).to receive(:user_registration_email).with(user: subject).and_return(mail)
expect(mail).to receive(:deliver_later)
end
end
end
But it is not working. I get this error:
Failure/Error: expect(UserConfirmationMailer).to receive(:user_registration_email).with(user: subject).and_return(mail)
(UserConfirmationMailer (class)).user_registration_email({:user=>#<User id: 5, email: "factory10#test.io", created_at: "2023-02-18 01:09:34.878424000 +0000", ...878424000 +0000", jti: "2163d284-1349-4e48-8a2a-1b52b578921c", username: "jose_test10", icon_id: 5>})
expected: 1 time with arguments: ({:user=>#<User id: 5, email: "factory10#test.io", created_at: "2023-02-18 01:09:34.878424000 +0000", ...878424000 +0000", jti: "2163d284-1349-4e48-8a2a-1b52b578921c", username: "jose_test10", icon_id: 5>})
received: 0 times
Am I doing something wrong when testing the action of sending the email in a callback?
PD.
My environment/test.rb config is as follows:
config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :test
config.action_mailer.default_url_options = { :host => "http://localhost:3000" }
Even if I change config.active_job.queue_adapter to :test following this, I get the same error.
Another way I am trying to do it is like this:
expect { FactoryBot.create(:valid_user) }.to have_enqueued_job(ActionMailer::MailDeliveryJob).with('UserConfirmationMailer', 'user_registration_email', 'deliver_now', subject)
But then subject and the user created in FactoryBot.create(:valid_user) is different..
Any ideas are welcome. Thank you!
The stubbing in the question doesn't work because it doesn't stub the chain of methods in the same order then they are called in the implementation.
When you want to stub this method chain
UserConfirmationMailer.with(user: self).user_registration_email.deliver_later
then you have to first stub the with call, then the user_registration_email and last the deliver_later.
describe '#save' do
subject(:user) { build(:valid_user) }
let(:parameterized_mailer) { instance_double('ActionMailer::Parameterized::Mailer') }
let(:parameterized_message) { instance_double('ActionMailer::Parameterized::MessageDelivery') }
before do
allow(UserConfirmationMailer).to receive(:with).and_return(parameterized_mailer)
allow(parameterized_mailer).to receive(:user_registration_email).and_return(parameterized_message)
allow(parameterized_message).to receive(:deliver_later)
end
context 'when user is created' do
it 'sends a welcome email' do
user.save!
expect(UserConfirmationMailer).to have_received(:with).with(user: user)
expect(parameterized_mailer).to have_received(:user_registration_email)
expect(parameterized_message).to have_received(:deliver_later)
end
end
end
Note: I am not sure if using instance_double will work in this case, because the Parameterized uses method_missing internally. Although instance_double is usually preferred, you might need to use double instead.

Rspec test call method send reconfirmation instruction

I’m using Rspec to test the case when user change password, mail will be sent. And I want to check that only 1 mail is sent. I don't want use Action::Mailer.deliveries to check, instead I want to check that whether method is called and how much.
On searching I found Test Spy from rspec mock:
https://github.com/rspec/rspec-mocks#test-spies
describe 'PUT /email' do
include_context 'a user has signed in', { email: 'old-email#example.com', password: 'correct_password' }
context 'correct new email, correct password' do
before do
allow(user).to receive(:send_reconfirmation_instructions)
end
it do
should == 302
expect(user.reload.unconfirmed_email).to eq 'new-email#example.com'
expect(user).to receive(:send_reconfirmation_instructions).once
end
end
end
But I got error:
Failure/Error: expect(user).to receive(:send_reconfirmation_instructions)
(#<User id: 1269, email: “old-email#example.com”, created_at: “2019-08-27 03:54:33", updated_at: “2019-08-27 03:54:33”...“>).send_reconfirmation_instructions(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
send_reconfirmation_instructions this function is from devise: https://github.com/plataformatec/devise/blob/master/lib/devise/models/confirmable.rb#L124-L130
I did binding.pry and I’m sure that the test jump inside this function but rspec still failed.
Edit:
I could make it kind of work by writing like this:
describe 'PUT /email' do
include_context 'a user has signed in', { email: 'old-email#example.com', password: 'correct_password' }
context 'correct new email, correct password' do
before do
expect_any_instance_of(User).to receive(:send_reconfirmation_instructions).once
end
it do
should == 302
expect(user.reload.unconfirmed_email).to eq 'new-email#example.com'
# expect(user).to receive(:send_reconfirmation_instructions).once
end
end
end
However I got another error:
Failure/Error:
(#<User user_id: 1418, email: “old-email#example.com”...“>).send_reconfirmation_instructions(#<User user_id: 1418, email: “old-email#example.com” ...“>)
expected: 1 time with any arguments
received: 2 times with arguments: (#<User user_id: 1418, email: “old-email#example.com”, id: 1323...“>)
The line where you're checking that the user has received the message should read:
expect(user).to have_received(:send_reconfirmation_instructions).once
instead of:
expect(user).to receive(:send_reconfirmation_instructions).once
The former where you say expect(obj).to have_received(:msg) requires you to assert that the message was called, as you intended to do.
The latter, on the other hand, where you say expect(obj).to receive(:msg) is a way to set up the expectation before the action, i.e. in lieu of the allow(obj).to receive(:msg) without requiring to assert that it was called after the action. After the spec ran, it will automatically assert whether it was called.
This explains the error you're getting when specifying
expect(user).to receive(:send_reconfirmation_instructions).once
as no code after that line sends that message to user, which gets verified after the spec.

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

Failure/Error: expected: 1 time with any arguement, recieved: 0 times with any arguments

I have a below spec, where i am mocking my user model and stubbing its method.
require 'spec_helper'
describe User do
let(:username) {"test#test.com"}
let(:password) {"123"}
let(:code) {"0"}
context "when signing in" do
let(:expected_results) { {token:"123"}.to_json }
it "should sign in" do
expect(User).to receive(:login).with({email: username, password: password, code: code})
.and_return(expected_results)
end
end
end
I get the below error, when i try to run my test case.
Failure/Error: expect(User).to receive(:login).with({email: username, password: password, code: code})
(<User (class)>).login({:email=>"test#test.com", :password=>"123", :code=>"0"})
expected: 1 time with arguments: ({:email=>"test#test.com", :password=>"123", :code=>"0"})
received: 0 times
You are misunderstanding what expect is.
expect(x).to receive(:y) is stubbing out the y method on the x.
ie, it is papering over that method.
A way of describing this would be that you are making an "expectation that method y will be called on x when you actually run your code"
Right now, you spec doesn't call any actual code... it just sets up the expectation... then stops.
If you are testing the method login then you need to not stub it out with an expectation, but actually call it for real.
eg User.login(email: username, password: password, code: code)
You currently don't actually have a test at all. Just a stub that you set up, and then never use.
As #taryn-east mentioned, you aren't actually testing the User.login method.
You likely want something like this:
it "should sign in" do
expect(User).to receive(:login).with({email: username, password: password, code: code})
.and_return(expected_results)
User.login(email: username, password: password, code: code)
end
Still Having Issues?
It may be worth checking where you are calling the expect method. Here's a mistake that gave me a headache:
# ❌ Failure/Error: expect(#notification_service).to receive(:send_notification)
# (ClassDouble(NotificationService) (anonymous)).send_notification(*(any args))
# expected: 1 time with any arguments
# received: 0 times with any arguments
expect(#notification_service).to receive(:send_notification)
post :create, :params => { :hub_id => 123, :notification => notification_data }
When it should have been:
# ✅
post :create, :params => { :hub_id => 123, :notification => notification_data }
expect(#notification_service).to receive(:send_notification)

Using RSpec to test for correct order of records in a model

I'm new to rails and RSpec and would like some pointers on how to get this test to work.
I want emails to be sorted from newest to oldest and I'm having trouble testing this.
I'm new to Rails and so far I'm having a harder time getting my tests to work then the actual functionality.
Updated
require 'spec_helper'
describe Email do
before do
#email = Email.new(email_address: "user#example.com")
end
subject { #email }
it { should respond_to(:email_address) }
it { should respond_to(:newsletter) }
it { should be_valid }
describe "order" do
#email_newest = Email.new(email_address: "newest#example.com")
it "should have the right emails in the right order" do
Email.all.should == [#email_newest, #email]
end
end
end
Here is the error I get:
1) Email order should have the right emails in the right order
Failure/Error: Email.all.should == [#email_newest, #email]
expected: [nil, #<Email id: nil, email_address: "user#example.com", newsletter: nil, created_at: nil, updated_at: nil>]
got: [] (using ==)
Diff:
## -1,3 +1,2 ##
-[nil,
- #<Email id: nil, email_address: "user#example.com", newsletter: nil, created_at: nil, updated_at: nil>]
+[]
# ./spec/models/email_spec.rb:32:in `block (3 levels) in <top (required)>'
In your code:
it "should have the right emails in the right order" do
Email.should == [#email_newest, #email]
end
You are setting the expectation that the Email model should be equal to the array of emails.
Email is a class. You can't just expect the class to be equal to an array. All emails can be found by using all method on class Email.
You must set the expectation for two arrays to be equal.
it "should have the right emails in the right order" do
Email.order('created_at desc').all.should == [#email_newest, #email]
end
It should work like this.
For newer version of RSpec:
let(:emails) { ... }
it 'returns emails in correct order' do
expect(emails).to eq(['1', '2', '3'])
end

Resources