Ruby on Rails - Testing Mailer as a callback in Model - ruby-on-rails

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.

Related

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

Rails doesn't recognise my app/services (uninitialized constant)

I am stuck and even if I found some subjects about this issue, I didn't found any solution.
I am trying to add a subscription to Mailchimp if my "Packer" register to our newsletter (A "Packer" is a kind of "User" - "User" only has the Devise parameters and "Packer" has the rest)
The error is an uninitialized constant, It seems to be because rails doesn't recognize my service.
Here is my #app/models/packer.rb
after_create :subscribe_to_newsletter
after_update :subscribe_to_newsletter
private
def subscribe_to_newsletter
SubscribeToNewsletterService.new.call(self.user) if self.newsletter
end
and my # app/services/subscribe_to_newsletter_service.rb
require "gibbon"
class SubscribeToNewsletterService
def initialize
#gibbon = Gibbon::Request.new(api_key: ENV['MAILCHIMP_API_KEY'])
#list_id = ENV['MAILCHIMP_NEWSLETTER_LIST_ID']
end
def call(user)
#gibbon.lists(#list_id).members.create(
body: {
email_address: user.email,
status: "subscribed",
double_optin: false,
# merge_fields: {
# FNAME: #user.first_name,
# LNAME: #user.last_name
# }
}
)
end
end
Looking into the different solutions, I also added that line in #config/application.rb
module Pyswebsitev1
class Application < Rails::Application
#config.autoload_paths += %W(#{config.root}/app/services)
config.i18n.default_locale = :en
end
end
When I do a rails console - ActiveSupport::Dependencies.autoload_paths
The result include /app/services
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/assets",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/channels",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/controllers",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/controllers/concerns",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/helpers",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/jobs",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/mailers",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/models",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/models/concerns",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/policies",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/app/services",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rails-assets-underscore-1.8.3/app/assets",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/jquery-fileupload-rails-0.4.7/app/assets",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/bundler/gems/attachinary-98a895be22ed/app/controllers",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rails_admin-1.2.0/app/assets",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rails_admin-1.2.0/app/controllers",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rails_admin-1.2.0/app/helpers",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/jquery-ui-rails-5.0.5/app/assets",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/font-awesome-rails-4.7.0.2/app/assets",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/font-awesome-rails-4.7.0.2/app/helpers",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/devise-4.3.0/app/controllers",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/devise-4.3.0/app/helpers",
"/usr/local/Cellar/rbenv/1.0.0/versions/2.3.1/lib/ruby/gems/2.3.0/gems/devise-4.3.0/app/mailers",
"/Users/MaxBook/code/PackYourSkills/pyswebsitev1/test/mailers/previews"
I also tried to "Spring stop", to reboot server, and to bundle.
If you have any idea, that would be very helpfull !
Thank you very much
Maybe you forgot to put the _service in your filename event if you put it in your question. Just guessing ...
Your forget the _service at the end of the filename.

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