Rspec test call method send reconfirmation instruction - ruby-on-rails

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.

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.

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 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)

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