Callback not getting called even though it should - ruby-on-rails

I have a model with a simple :after_update callback that is not firing for some reason. I have a test that checks whether the function is called and it passes, but for some reason nothing actually happens.
class HealthProfile < ApplicationRecord
after_update :send_office_email
def send_office_email
p "sending office email"
email = MailjetService.new(ID)
Rails.application.routes.default_url_options[:host] = 'host_name'
message = email.send_email('email', self.location.email, "New Health Profile: #{self.name}", {
"office_name" => self.location.name,
"name" => self.name,
"url" => Rails.application.routes.url_helpers.health_profile_url(self)
})
p message
end
end
Test:
require 'rails_helper'
RSpec.describe HealthProfile, type: :model do
before(:each) do
#hp = build(:health_profile)
end
it 'emails office on update (complete - w/ signature)' do
p 1
#hp.save
p 2
expect(#hp).to receive(:send_office_email)
p "updating signature to true..."
#hp.update(signature: 't')
p 3
end
end
In my test output I see the following:
1
2
"updating signature to true..."
3
But I never see sending office email. The test passes, showing that the model received the callback, but nothing in the callback ran. What am I missing?

Actually, I believe your test is working. If it wasn't your test would be failing with expected to receive "send_office_email" but nothing was called.
expect to_receive is stubbing the method on the object which results in a dummy method that doesn't call your original method.
Try this.
require 'rails_helper'
RSpec.describe HealthProfile, type: :model do
let(:hp) { build(:health_profile) }
it 'emails office on update (complete - w/ signature)' do
p 1
hp.save
p 2
p "updating signature to true..."
hp.update(signature: 't')
p 3
end
end

Related

Is it safe to modify classes for example using "class_eval" (Ruby) in the tests suite?

In other words: It there a chance for a class modification (on tests) to affect production code?
(This code example is using Rspec for testing in a Rails app)
My controller example
In this controller ExternalModel is created. Then it's "inscription" method is called and the results are assigned to a variable. It uses the result for other actions on the controller method.
class ExampleController < ApplicationController
def callback_page
external_model = ExternalModel.new(argument)
result = external_model.inscription
render_error_json && return unless result['error_desc'].eql? 'OK'
TransactionModel.create(token: result['token'])
end
end
My Spec example
In the spec I modify ExternalModel so it returns what I want when calling the .inscription method:
ExternalModel.class_eval {
def inscription(_fake_arguments)
{
'error_desc' => 'OK',
'token' => '1234'
}
end
}
This is the entire spec:
RSpec.describe 'Example management', type: :request do
context 'callback_page' do
it 'creates a transaction' do
ExternalModel.class_eval {
def inscription(_fake_arguments)
{
'error_desc' => 'OK',
'token' => '1234'
}
end
}
expect {
post(callback_page_path)
}.to change(TransactionModel.all, :count).by(1)
expect(response).to render_template(:callback_page)
end
end
end
What you're trying to achieve here is exactly what stubs are for: They're effectively a way to fake behavior within the scope of a single example that then automatically resets to its original behavior after the example has run.
In your example, this would look roughly like this:
allow_any_instance_of(ExternalModel).
to receive(:inscription).
and_return({ 'error_desc' => 'OK', 'token' => '1234' })
More details can be found in the docs for the rspec-mocks gem: https://relishapp.com/rspec/rspec-mocks/v/3-9/docs.

Testing custom validation method with RSpec

I try to test validation method that check times overlap for activities.
There are three factories(two of them inherit from activity).
Factories:
activities.rb
FactoryGirl.define do
factory :activity do
name 'Fit Girls'
description { Faker::Lorem.sentence(3, true, 4) }
active true
day_of_week 'Thusday'
start_on '12:00'
end_on '13:00'
pool_zone 'B'
max_people { Faker::Number.number(2) }
association :person, factory: :trainer
factory :first do
name 'Swim Cycle'
description 'Activity with water bicycles.'
active true
day_of_week 'Thusday'
start_on '11:30'
end_on '12:30'
end
factory :second do
name 'Aqua Crossfit'
description 'Water crossfit for evereyone.'
active true
day_of_week 'Thusday'
start_on '12:40'
end_on '13:40'
pool_zone 'C'
max_people '30'
end
end
end
Activities overlaps when are on same day_of_week(activity.day_of_week == first.day_of_week), on same pool_zone(activity.pool_zone == first.pool_zone) and times overlaps.
Validation method:
def not_overlapping_activity
overlapping_activity = Activity.where(day_of_week: day_of_week)
.where(pool_zone: pool_zone)
activities = Activity.where(id: id)
if activities.blank?
overlapping_activity.each do |oa|
if (start_on...end_on).overlaps?(oa.start_on...oa.end_on)
errors.add(:base, "In this time and pool_zone is another activity.")
end
end
else
overlapping_activity.where('id != :id', id: id).each do |oa|
if (start_on...end_on).overlaps?(oa.start_on...oa.end_on)
errors.add(:base, "In this time and pool_zone is another activity.")
end
end
end
end
I wrote rspec test, but unfortunatelly invalid checks.
describe Activity, 'methods' do
subject { Activity }
describe '#not_overlapping_activity' do
let(:activity) { create(:activity) }
let(:first) { create(:first) }
it 'should have a valid factory' do
expect(create(:activity).errors).to be_empty
end
it 'should have a valid factory' do
expect(create(:first).errors).to be_empty
end
context 'when day_of_week, pool_zone are same and times overlap' do
it 'raises an error that times overlap' do
expect(activity.valid?).to be_truthy
expect(first.valid?).to be_falsey
expect(first.errors[:base].size).to eq 1
end
end
end
end
Return:
Failure/Error: expect(first.valid?).to be_falsey
expected: falsey value
got: true
I can't understand why it got true. First create(:activity) should be right, but next shouldn't be executed(overlapping).
I tried add expect(activity.valid?).to be truthy before expect(first.valid?..., but throws another error ActiveRecord::RecordInvalid. Could someone repair my test? I'm newbie with creation tests using RSpec.
UPDATE:
Solution for my problem is not create :first in test but build.
let(:first) { build(:first) }
This line on its own
let(:activity) { create(:activity) }
doesn't create an activity. It only creates an activity, when activity is actually called. Therefore you must call activity somewhere before running your test.
There are several ways to do so, for example a before block:
before { activity }
or you could use let! instead of just let.

Testing an expected order of an array in RSpec / Rails

In a RSpec spec file I have the following test
it 'should return 5 players with ratings closest to the current_users rating' do
matched_players = User.find(:all,
:select => ["*,(abs(rating - current_user.rating)) as player_rating"],
:order => "player_rating",
:limit => 5)
# test that matched_players array returns what it is suppose to
end
How would I complete this to test that matched_players is returning the correct users.
I think you should first introduce some test users to the test DB (using for example a Factory) and afterwards see that the test is returning the correct ones.
Also it would make more sense to have a method in your model that would return the matched users.
For example:
describe "Player matching" do
before(:each) do
#user1 = FactoryGirl.create(:user, :rating => 5)
...
#user7 = FactoryGirl.create(:user, :rating => 3)
end
it 'should return 5 players with ratings closest to the current_users rating' do
matched_players = User.matched_players
matched_players.should eql [#user1,#user3,#user4,#user5,#user6]
end
end
Your model shouldn't know about your current user (the controllers know about this concept)
You need to extract this as a method on the User class otherwise there's no point in testing it, i.e. why test logic that isn't even in your app code?
The function that gets the matched players doesn't need to know about the current user, or any user for that matter, just the rating.
To test it, create a bunch of User instances, call the method, and see that the result is a list of the correct user instances you expect.
models/user.rb
class User < ActiveRecord::Base
...
def self.matched_players(current_user_rating)
find(:all,
select: ["*,(abs(rating - #{current_user_rating)) as match_strength"],
order: "match_strength",
limit: 5)
end
...
end
spec/models/user_spec.rb
describe User do
...
describe "::matched_players" do
context "when there are at least 5 users" do
before do
10.times.each do |n|
instance_variable_set "#user#{n}", User.create(rating: n)
end
end
it "returns 5 users whose ratings are closest to the given rating, ordered by closeness" do
matched_players = described_class.matched_players(4.2)
matched_players.should == [#user4, #user5, #user3, #user6, #user2]
end
context "when multiple players have ratings close to the given rating and are equidistant" do
# we don't care how 'ties' are broken
it "returns 5 users whose ratings are closest to the given rating, ordered by closeness" do
matched_players = described_class.matched_players(4)
matched_players[0].should == #user4
matched_players[1,2].should =~ [#user5, #user3]
matched_players[3,4].should =~ [#user6, #user2]
end
end
end
context "when there are fewer than 5 players in total" do
...
end
...
end
...
end

What is the difference between these two tests causing one to fail and one to pass?

I have the following two tests for a Rails app I'm working on.
This one fails:
test "should save comment without parent comment" do
comment = Comment.new(:text => "hello world")
comment.user = users(:charlie)
comment.story = stories(:one)
assert comment.save
end
And this one passes:
test "should save comment without parent comment" do
comment = Comment.new(:text => "hello world")
comment.user = users(:charlie)
comment.story = stories(:one)
comment.save
assert comment.save
end
When I change the assert line in the first (failing) test to this:
assert comment.save, Proc.new { "#{comment.errors.messages.inspect} -- valid? #{comment.valid?} -- save? #{comment.save}" }
It prints out this as the error message:
{} -- valid? true -- save? true
I'm completely at a loss as to why my Comment model requires two calls to save. It works fine in my controller with just one call to save.
I have an after_create callback, but I don't think it should be affecting anything (as I said, this all works fine from my controller):
after_create :create_upvote_from_user
def create_upvote_from_user
Vote.create! :user => user, :vote => 1, "#{self.class.name.underscore}_id" => id
end

Debugging Delayed_Jobs

I'm looking to debug a delayed jobs class. First off I added the config/initializers/delayed_job_config to move my logging to my log/production.rb file.
Delayed::Job.destroy_failed_jobs = false
Delayed::Worker.logger = Rails.logger
Then in the actual file I'm doing in the actual file
class TestJob < Struct.new()
logger.debug("test logging")
end
The log isn't showing anything in it. Anyone have any ideas?
I've had luck with rending the backtrace of the error to an email, which at least gives me some context of when / how the delayed job is failing:
Here is an example:
result = Delayed::Job.work_off
unless result[1].zero?
ExceptionMailer.deliver_exception_message("[Delayed Job Failed] Error id: #{Delayed::Job.last.id}", Delayed::Job.last.last_error)
end
If you just want to write tests for your Delayed::Job tasks here is the approach I have taken. I will stub out the perform task with the expectations from the various scenarios and then test how Delayed::Job handles those results. Here is an example of how I used Delayed::Job to sync with a remote CMS nightly.
it "should sync content from the remote CMS" do
CMSJobs::Sync.module_eval do
def perform
url.should == "http://someurl.com/tools/datafeed/resorts/all"
Resort.sync_resorts!([{'id' => 1, 'name' => 'resort foo'}, { 'id' => 2, 'name' => 'resort bar' }])
end
end
lambda do
Resort.sync_all!
end.should change(Delayed::Job, :count)
lambda do
Delayed::Job.work_off
end.should change(Resort, :count).by(2)
# It should now delete a resort if it doesn't appear in the next CMS feed.
lambda do
Resort.sync_resorts!([{ 'id' => 2, 'name' => 'resort bar' }])
end.should change(Resort, :count).by(-1)
end
Have you tried to narrow it down to whether this is a problem with the delayed job not getting fired or the logger config not working for you? What if you replace the logger.debug call with a puts?

Resources