Rspec testing callback method - ruby-on-rails

I have a model with the following callback:
class CheckIn < ActiveRecord::Base
after_create :send_delighted_survey
def send_delighted_survey
position = client&.check_ins&.reverse&.index(self)
if position.present? && type_of_weighin.present?
survey = SurveyRequirement.find_by(position: [position, "*"], type_of_weighin: [type_of_weighin, "*"])
if survey.present?
survey.delighted_survey.sendSurvey(client: self.client, additional_properties: {delay: 3600})
end
end
end
end
I am attempting to test the line: survey.delighted_survey.sendSurvey(client: self.client, additional_properties: {delay: 3600}) to ensure that the correct delighted_survey is receiving sendSurvey.
This test passes:
let!(:week_1_sr) { create(:survey_requirement, :week_1_survey) }
it "should fire a CSAT survey after week 1" do
expect_any_instance_of(DelightedSurvey).to receive(:sendSurvey).once
create(:check_in, client_id: client.id, type_of_weighin: "standard")
create(:check_in, client_id: client.id, type_of_weighin: "standard")
end
However this test fails and I don't understand why
let!(:week_1_sr) { create(:survey_requirement, :week_1_survey) }
it "should fire a CSAT survey after week 1" do
expect(week_1_sr.delighted_survey).to receive(:sendSurvey).once
create(:check_in, client_id: client.id, type_of_weighin: "standard")
create(:check_in, client_id: client.id, type_of_weighin: "standard")
end
When I add print statements, it's definitely calling sendsurvey on week_1_sr.delighted_survey so I don't understand why the test fails.
How should I rearrange this test?

In my experience this is a common misunderstanding with expect(..).to receive when dealing with ActiveRecord.
We have to remember that the ActiveRecord object we create in our test stores a row in the database, the code in your model loads the row from the database and populates an entirely different activerecord object that is completely unconnected from the one in your test, except that they both refer to the same underlying database row.
Rspec is not "smart" about activerecord and the method you're stubbing/expecting is only on the instance of object in your test.
So how to fix this. The most direct option is to stub out the object that your code actually uses. This isn't easy, however, since it's an object returned from a method on another object returned by SurveyRequirement.find_by(...). You can do it with something like:
Option 1 - Stub everything
survey_requirement_stub = double(SurveyRequirement)
survey_stub = double(Survey)
allow(SurveyRequirement).to receive(:find_by).and_return(survey_requirement_stub)
allow(survey_requirement_stub).to receive(:delighted_survey).and_return(survey_stub)
expect(survey_stub).to receive(:sendSurvey).once
However I wouldn't recommend this. It closely ties your test to the internal implementation of your method. e.g. Adding a scope (scoped.find_by instead of find_by) breaks the test in a way that's not meaningful.
Option 2 - Test the results, not the implementation
If the point of sendSurvey is enqueuing a background job or sending an email, that may be be a better place to test that it's doing what's expected, for example:
expect { create_checkin }.to have_enqueued_job(MyEmailJob)
# or, if it sends right away
expect { create_checkin }.to change { ActionMailer::Base.deliveries.count }.by(1)
I think this approach is OK, but the implementation means that your code will be enqueuing jobs and firing emails throughout your test base. It will not be possible to create a check-in and not fire these.
This is why I strongly advise our engineers to NEVER use activerecord callbacks for business logic like this.
Instead...
Option 3 - Refactor to use service objects / interactors instead
As your application grows, using activerecord callbacks to create other records, update records, or trigger side-effects (like emails) becomes a significant anti-pattern. I'd take this as an opportunity to restructure the code to be easier to test and remove business logic from your ActiveRecord objects.
This should make each part of this easier to test (e.g. is the survey-requirement looked up correctly? Does it send?). I've run out of time but here's the general idea:
class CheckIn
def get_survey_requirement
position = client&.check_ins&.reverse&.index(self)
return unless position.present? && type_of_weighin.present?
SurveyRequirement.find_by(position: [position, "*"], type_of_weighin: [type_of_weighin, "*"])
end
end
class CheckInCreater
def self.call(params)
check_in = CheckIn.build(params)
check_in.save!
DelightedSurveySender.call(check_in)
end
end
class DelightedSurveySender
def self.call(check_in)
survey = check_in.survey_requirement&.delighted_survey
return unless survey
survey.send_survey(client: check_in.client, additional_properties: {delay: 3600})
end
end

This is happening because week_1_sr.delighted_survey in spec and survey.delighted_survey aren't the same instance. Yes, both are instances of the same class and they both represent the same record in the database and same model behavior, but they do not have the same object_id.
In your first test, you are expecting that any instance of DelightedSurvey receives the method and that's, indeed, true. But in your second spec, you expect that that exact instance receives sendSurvey.
There are many ways to rearrange your test. In fact, if you ask 100 developers how to test something, you will get 100 different answers.
There is this approach:
let(:create_week_1_sr) { create(:survey_requirement, :week_1_survey) }
it "should fire a CSAT survey after week 1" do
week_1_sr = create_week_1_sr # I don't think DRY is the better approach for testing, but it's just my opinion
allow(SurveyRequirement).to(receive(:find_by).and_return(week_1_sr))
delighted_survey_spy = instance_spy(DelightedSurvey)
allow(week_1_sr).to(receive(:delighted_survey).and_return(delighted_survey_spy))
create(:check_in, client_id: client.id, type_of_weighin: "standard")
create(:check_in, client_id: client.id, type_of_weighin: "standard")
expect(delighted_survey_spy).to(have_received(:sendSurvey))
end
First thing about the test I wrote: Arrange, Act and Assert. It's clear to me where I am arranging my test, where I am acting and where I am asserting.
But you can realize that this test is polluted and has some prejudicial mocks. Like:
allow(SurveyRequirement).to(receive(:find_by).and_return(week_1_sr))
It will return week_1_sr even if you pass a wrong parameter to find_by (you can workaround it using with, but it will add logic to your tests).
You can see that it's pretty hard to test and I would agree. So would you consider removing this logic to a service class or whatever?
Oh, and just a heads up: after_create will be triggered even if the record is not commited for whatever reason. So you might consider using after_create_commit
(just finished and got the notice of melcher's answer. his is better)

Related

How do I test if a method is called on particular objects pulled from the DB in Rails with RSpec?

If I have a User model that includes a method dangerous_action and somewhere I have code that calls the method on a specific subset of users in the database like this:
class UserDanger
def perform_dangerous_action
User.where.not(name: "Fred").each(&:dangerous_action)
end
end
how do I test with RSpec whether it's calling that method on the correct users, without actually calling the method?
I've tried this:
it "does the dangerous thing, but not on Fred" do
allow_any_instance_of(User).to receive(:dangerous_action).and_return(nil)
u1 = FactoryBot.create(:user, name: "Jill")
u2 = FactoryBot.create(:user, name: "Fred")
UserDanger.perform_dangerous_action
expect(u1).to have_recieved(:dangerous_action)
expect(u2).not_to have_recieved(:dangerous_action)
end
but, of course, the error is that the User object doesn't respond to has_recieved? because it's not a double because it's an object pulled from the database.
I think I could make this work by monkey-patching the dangerous_action method and making it write to a global variable, then check the value of the global variable at the end of the test, but I think that would be a really ugly way to do it. Is there any better way?
I realised that I'm really trying to test two aspects of the perform_dangerous_action method. The first is the scoping of the database fetch, and the second is that it calls the correct method on the User objects that come up.
For testing the scoping of the DB fetch, I should really just make a scope in the User class:
scope :not_fred, -> { where.not(name: "Fred") }
which can be easily tested with a separate test.
Then the perform_dangerous_action method becomes
def perform_dangerous_action
User.not_fred.each(&:dangerous_action)
end
and the test to check it calls the right method for not_fred users is
it "does the dangerous thing" do
user_double = instance_double(User)
expect(user_double).to receive(:dangerous_action)
allow(User).to receive(:not_fred).and_return([user_double])
UserDanger.perform_dangerous_action
end
i think, in many cases, you don't want to separate a where or where.not into a scope, in that cases, you could stub ActiveRecord::Relation itself, such as:
# default call_original for all normal `where`
allow_any_instance_of(ActiveRecord::Relation)
.to receive(:where).and_call_original
# stub special `where`
allow_any_instance_of(ActiveRecord::Relation)
.to receive(:where).with(name: "...")
.and_return(user_double)
in your case, where.not is actually call ActiveRecord::QueryMethods::WhereChain#not method so i could do
allow_any_instance_of(ActiveRecord::QueryMethods::WhereChain)
.to receive(:not).with(name: "Fred")
.and_return(user_double)

Find last created record RSpec test

How could I write a test to find the last created record?
This is the code I want to test:
Post.order(created_at: :desc).first
I'm also using factorybot
If you've called your method 'last_post':
def self.last_post
Post.order(created_at: :desc).first
end
Then in your test:
it 'should return the last post' do
expect(Post.last_post).to eq(Post.last)
end
On another note, the easiest way to write your code is simply
Post.last
And you shouldn't really be testing the outcome of ruby methods (you should be making sure the correct ruby methods are called), so if you did:
def self.last_post
Post.last
end
Then your test might be:
it 'should send the last method to the post class' do
expect(Post).to receive(:last)
Post.last_post
end
You're not testing the outcome of the 'last' method call - just that it gets called.
The accepted answer is incorrect. Simply doing Post.last will order the posts by the ID, not by when they were created.
https://apidock.com/rails/ActiveRecord/FinderMethods/last
If you're using sequential IDs (and ideally you shouldn't be) then obviously this will work, but if not then you'll need to specify the column to sort by. So either:
def self.last_post
order(created_at: :desc).first
end
or:
def self.last_post
order(:created_at).last
end
Personally I'd look to do this as a scope rather than a dedicated method.
scope :last_created -> { order(:created_at).last }
This allows you to create some nice chains with other scopes, such as if you had one to find all posts by a particular user/account, you could then chain this pretty cleanly:
Post.for_user(user).last_created
Sure you can chain methods as well, but if you're dealing with Query interface methods I feel scopes just make more sense, and tend to be cleaner.
If you wanted to test that it returns the correct record, in your test you could do something like:
let!(:last_created_post) { factory_to_create_post }
. . .
it "returns the correct post"
expect(Post.last_post).to eq(last_created_post)
end
If you wanted to have an even better test, you could create a couple records before the last record to verify the method under test is pulling the correct result and not just a result from a singular record.

How to test correlated (coupled) methods in RSpec?

Let's say we have class:
class Post
def save
# implementation
end
def self.find(id)
#implementation
end
end
I struggle with testing #save and .find, I've:
describe '#save' do
it 'saves the post' do
subject.save
created = Post.find(subject.id)
expect(created).to eq(subject)
end
end
describe '.find' do
it 'finds the post' do
subject.save
created = Post.find(subject.id)
expect(created).to eq(subject)
end
end
In case of #save method I'd like to check side effect, in case of .find I'd like to test returned value. How to cope with this case without duplicating specs ?
In this case, to isolate the save and find actions, you need to mock the repository.
Whether you are writing to a DB, a file-system, cache, or whatever - you can mock it to either expect the saving feature, or set it up (before the beginning of the test) to make sure find works.
For most repository implementations there are gems to mock them (Factory Girl for relational databases, FakeFS for file-system), but you can roll your own if you have some exotic repository no one has heard of.
This way you test save without using find, or vice versa.

Stubbing out ActiveRecord models in Service tests

I'm following a TDD approach to building our app, and creating a whole bunch of service objects, keeping models strictly for data management.
Many of the services I've built interface with models. Take for example MakePrintsForRunner:
class MakePrintsForRunner
def initialize(runner)
#runner = runner
end
def from_run_report(run_report)
run_report.photos.each do |photo|
Print.create(photo: photo, subject: #runner)
end
end
end
I appreciate the create method could arguably be abstracted into the Print model, but let's keep it as is for now.
Now, in the spec for MakePrintsForRunner I'm keen to avoid including spec_helper, since I want my service specs to be super fast.
Instead, I stub out the Print class like this:
describe RunnerPhotos do
let(:runner) { double }
let(:photo_1) { double(id: 1) }
let(:photo_2) { double(id: 2) }
let(:run_report) { double(photos: [photo_1, photo_2]) }
before(:each) do
#service = RunnerPhotos.new(runner)
end
describe "#create_print_from_run_report(run_report)" do
before(:each) do
class Print; end
allow(Print).to receive(:create)
#service.create_print_from_run_report(run_report)
end
it "creates a print for every run report photo associating it with the runners" do
expect(Print).to have_received(:create).with(photo: photo_1, subject: runner)
expect(Print).to have_received(:create).with(photo: photo_2, subject: runner)
end
end
end
And all goes green. Perfect!
... Not so fast. When I run the whole test suite, depending on the seed order, I am now running into problems.
It appears that the class Print; end line can sometimes overwrite print.rb's definition of Print (which obviously inherits from ActiveRecord) and therefore fail a bunch of tests at various points in the suite. One example is:
NoMethodError:
undefined method 'reflect_on_association' for Print:Class
This makes for an unhappy suite.
Any advice on how to tackle this. While this is one example, there are numerous times where a service is directly referencing a model's method, and I've taken the above approach to stubbing them out. Is there a better way?
You don't have to create the Print class, simply use the one that is loaded, and stub it:
describe RunnerPhotos do
let(:runner) { double }
let(:photo_1) { double(id: 1) }
let(:photo_2) { double(id: 2) }
let(:run_report) { double(photos: [photo_1, photo_2]) }
before(:each) do
#service = RunnerPhotos.new(runner)
end
describe "#create_print_from_run_report(run_report)" do
before(:each) do
allow(Print).to receive(:create)
#service.create_print_from_run_report(run_report)
end
it "creates a print for every run report photo associating it with the runners" do
expect(Print).to have_received(:create).with(photo: photo_1, subject: runner)
expect(Print).to have_received(:create).with(photo: photo_2, subject: runner)
end
end
end
Edit
If you really need to create the class in the scope of this test alone, you can undefine it at the end of the test (from How to undefine class in Ruby?):
before(:all) do
unless Object.constants.include?(:Print)
class TempPrint; end
Print = TempPrint
end
end
after(:all) do
if Object.constants.include?(:TempPrint)
Object.send(:remove_const, :Print)
end
end
I appreciate the create method could arguably be abstracted into the Print model, but let's keep it as is for now.
Let's see what happens if we ignore this line.
Your difficulty in stubbing a class is a sign that the design is inflexible. Consider passing an already-instantiated object to either the constructor of MakePrintsForRunner or the method #from_run_report. Which to choose depends on the permanence of the object - will the configuration of printing need to change at run time? If not, pass to the constructor, if so, pass to the method.
So for our step 1:
class MakePrintsForRunner
def initialize(runner, printer)
#runner = runner
#printer = printer
end
def from_run_report(run_report)
run_report.photos.each do |photo|
#printer.print(photo: photo, subject: #runner)
end
end
end
Now it's interesting that we're passing two objects to the constructor, yet #runner is only ever passed to the #print method of #printer. This could be a sign that #runner doesn't belong here at all:
class MakePrints
def initialize(printer)
#printer = printer
end
def from_run_report(run_report)
run_report.photos.each do |photo|
#printer.print(photo)
end
end
end
We've simplified MakePrintsForRunner into MakePrints. This only takes a printer at construction time, and a report at method invocation time. The complexity of which runner to use is now the responsibility of the new 'printer' role.
Note that the printer is a role, not necessarily a single class. You can swap the implementation for different printing strategies.
Testing should now be simpler:
photo1 = double('photo')
photo2 = double('photo')
run_report = double('run report', photos: [photo1, photo2])
printer = double('printer')
action = MakePrints.new(printer)
allow(printer).to receive(:print)
action.from_run_report(run_report)
expect(printer).to have_received(:print).with(photo1)
expect(printer).to have_received(:print).with(photo2)
These changes might not suit your domain. Perhaps a runner shouldn't be attached to a printer for more than one print. In this case, perhaps you should take a different next step.
Another future refactoring might be for #from_run_report to become #from_photos, since the report isn't used for anything but gathering photos. At this point the class looks a bit anaemic, and might disappear altogether (eaching over photos and calling #print isn't too interesting).
Now, how to test a printer? Integrate with ActiveRecord. This is your adapter to the outside world, and as such should be integration tested. If all it really does is create a record, I probably wouldn't even bother testing it - it's just a wrapper around an ActiveRecord call.
Class names are just constants so you could use stub_const to stub an undefined constant and return a double.
So instead of defining a class in your before(:each) block do this:
before(:each) do
stub_const('Print', double(create: nil))
#service.create_print_from_run_report(run_report)
end

How can I test that my before_save callback does the right thing

I have a callback on my ActiveRecord model as shown below:
before_save :sync_to_external_apis
def sync_to_external_apis
[user, assoc_user].each {|cuser|
if cuser.google_refresh
display_user = other_user(cuser.id)
api = Google.new(:user => cuser)
contact = api.sync_user(display_user)
end
}
end
I would like to write an rspec test which tests that calling save! on an instance of this model causes sync_user to be called on a new Google instance when google_refresh is true. How could I do this?
it "should sync to external apis on save!" do
model = Model.new
model.expects(:sync_to_external_apis)
model.save!
end
As an aside, requesting unreliable resources like the internet during the request-response cycle is a bad idea. I would suggest creating a background job instead.
The usual method for testing is to ensure the results are as expected. Since you're using an API in this case that may complicate things. You may find that using mocha to create a mock object you can send API calls would allow you to substitute the Google class with something that works just as well for testing purposes.
A simpler, yet clunkier approach is to have a "test mode" switch:
def sync_to_external_apis
[ user, assoc_user ].each do |cuser|
if (Rails.env.test?)
#synced_users ||= [ ]
#synced_users << cuser
else
# ...
end
end
end
def did_sync_user?(cuser)
#synced_users and #synced_users.include?(cuser)
end
This is a straightforward approach, but it will not validate that your API calls are being made correctly.
Mocha is the way to go. I'm not familiar with rspec, but this is how you would do it in test unit:
def test_google_api_gets_called_for_user_and_accoc_user
user = mock('User') # define a mock object and label it 'User'
accoc_user = mock('AssocUser') # define a mock object and label it 'AssocUser'
# instantiate the model you're testing with the mock objects
model = Model.new(user, assoc_user)
# stub out the other_user method. It will return cuser1 when the mock user is
# passed in and cuser2 when the mock assoc_user is passed in
cuser1 = mock('Cuser1')
cuser2 = mock('Cuser2')
model.expects(:other_user).with(user).returns(cuser1)
model.expects(:other_user).with(assoc_user).returns(cuser2)
# set the expectations on the Google API
api1 - mock('GoogleApiUser1') # define a mock object and lable it 'GoogleApiUser1'
api2 - mock('GoogleApiUser2') # define a mock object and lable it 'GoogleApiUser2'
# call new on Google passing in the mock user and getting a mock Google api object back
Google.expects(:new).with(:user => cuser1).returns(api1)
api1.expects(:sync_user).with(cuser1)
Google.expects(:new).with(:user => cuser2).returns(api2)
api2.expects(:sync_user).with(cuser2)
# now execute the code which should satisfy all the expectations above
model.save!
end
The above may seem complicated, but it's not once you get the hang of it. You're testing that when you call save, your model does what it is supposed to do, but you don't have the hassle, or time expense of really talking to APIs, instantiating database records, etc.

Resources