How to test correlated (coupled) methods in RSpec? - ruby-on-rails

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.

Related

Rspec testing callback method

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)

RSpec Tests For Method Return & Inheritance

I am trying to write two RSpec tests for two different problems that are much more advanced that what I'm used to writing.
What I'm trying to test within my controller:
def index
#buildings ||= building_class.active.where(place: current_place)
end
My attempt at writing the RSpec test:
describe 'GET :index' do
it "assigns #buildings" do
#buildings ||= building_class.active.where(place: current_place)
get :index
expect(assigns(:buildings)).to eq([building])
end
end
This test failed and wouldn't even run so I know I'm missing something.
My second test is needing to test the returned value of a class method. Here is what I am needing to test within the controller:
def class_name
ABC::Accountant::Business
end
Here is my attempt at testing this method:
describe "class name returns ABC::Accountant::Business" do
subject do
expect(subject.class_name).to eq(ABC::Accountant::Business)
end
end
For the first test I would do something like this:
First, I would move that .active.where(place: current_place) to a scope (I'm guessing building_class returns Building or something like that):
class Building << ApplicationRecord
scope :active_in, -> (place) { active.where(place: place)
Then it's easier to stub for the test
describe 'GET :index' do
it "assigns #buildings" do
scoped_buildings = double(:buildings)
expect(Building).to receive(:active_in).and_return(scoped_buildings)
get :index
expect(assigns(:buildings)).to eq(scoped_buildings)
end
end
Then your controller will do
#buildings ||= building_class.active_in(current_place)
This way you are testing two things: that the controller actually calls the scope and that the controller assigns the returned value on the #buildings variable (you don't really need to test the actual buidlings, you can test the scope on the model spec).
Personally, I feel like it would be better to do something like #buildings = current_place.active_buildings using the same idea of the scope to test that you are getting the active buildings of the current place.
EDIT: if you can't modify your controller, then the stubbing is a little different and it implies some chaining of methods that I don't like to explicitly test.
scoped_buildings = double(:buildings)
controller.stub_chain(:building_class, :active, :where).and_return(scoped_building)
get :index
expect(assings(:buildings)).to eq scoped_buildings
Note that now your test depends on a specific implementation and testing implementation is a bad practice, one should test behaviour and not implementation.
For the second, I guess something like this should work:
describe ".class_name" do
it "returns ABC::Accountant::Business" do
expect(controller.class_name).to eq(ABC::Accountant::Business)
end
end
IMHO, that the method's name if confusing, class_name gives the idea that it returns a string, you are not returnin a name, you are returning a class. Maybe you can change that method to resource_class or something less confusing.

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

Best practice for reusing code in Rspec?

I'm writing integration tests using Rspec and Capybara. I've noticed that quite often I have to execute the same bits of code when it comes to testing the creation of activerecord options.
For instance:
it "should create a new instance" do
# I create an instance here
end
it "should do something based on a new instance" do
# I create an instance here
# I click into the record and add a sub record, or something else
end
The problem seems to be that ActiveRecord objects aren't persisted across tests, however Capybara by default maintains the same session in a spec (weirdness).
I could mock these records, but since this is an integration test and some of these records are pretty complicated (they have image attachments and whatnot) it's much simpler to use Capybara and fill out the user-facing forms.
I've tried defining a function that creates a new record, but that doesn't feel right for some reason. What's the best practice for this?
There are a couple different ways to go here. First of all, in both cases, you can group your example blocks under either a describe or context block, like this:
describe "your instance" do
it "..." do
# do stuff here
end
it "..." do
# do other stuff here
end
end
Then, within the describe or context block, you can set up state that can be used in all the examples, like this:
describe "your instance" do
# run before each example block under the describe block
before(:each) do
# I create an instance here
end
it "creates a new instance" do
# do stuff here
end
it "do something based on a new instance" do
# do other stuff here
end
end
As an alternative to the before(:each) block, you can also use let helper, which I find a little more readable. You can see more about it here.
The very best practice for your requirements is to use Factory Girl for creating records from a blueprint which define common attributes and database_cleaner to clean database across different tests/specs.
And never keep state (such as created records) across different specs, it will lead to dependent specs. You could spot this kind of dependencies using the --order rand option of rspec. If your specs fails randomly you have this kind of issue.
Given the title (...reusing code in Rspec) I suggest the reading of RSpec custom matchers in the "Ruby on Rails Tutorial".
Michael Hartl suggests two solutions to duplication in specs:
Define helper methods for common operations (e.g. log in a user)
Define custom matchers
Use these stuff help decoupling the tests from the implementation.
In addition to these I suggest (as Fabio said) to use FactoryGirl.
You could check my sample rails project. You could find there: https://github.com/lucassus/locomotive
how to use factory_girl
some examples of custom matchers and macros (in spec/support)
how to use shared_examples
and finally how to use very nice shoulda-macros
I would use a combination of factory_girl and Rspec's let method:
describe User do
let(:user) { create :user } # 'create' is a factory_girl method, that will save a new user in the test database
it "should be able to run" do
user.run.should be_true
end
it "should not be able to walk" do
user.walk.should be_false
end
end
# spec/factories/users.rb
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
username { Faker::Internet.user_name }
end
end
This allows you to do great stuff like this:
describe User do
let(:user) { create :user, attributes }
let(:attributes) { Hash.new }
it "should be able to run" do
user.run.should be_true
end
it "should not be able to walk" do
user.walk.should be_false
end
context "when user is admin" do
let(:attributes) { { admin: true } }
it "should be able to walk" do
user.walk.should be_true
end
end
end

What's a nice way to verify within a unit test that an ActiveRecord transaction is being used?

I have a class which performs several database operations, and I want to write a unit test which verifies that these operations are all performed within a transaction. What's a nice clean way to do that?
Here's some sample code illustrating the class I'm testing:
class StructureUpdater
def initialize(structure)
#structure = structure
end
def update_structure
SeAccount.transaction do
delete_existing_statistics
delete_existing_structure
add_campaigns
# ... etc
end
end
private
def delete_existing_statistics
# ...
end
def delete_existing_structure
# ...
end
def add_campaigns
# ...
end
end
Rspec lets you assert that data has changed in the scope of a particular block.
it "should delete existing statistics" do
lambda do
#structure_updater.update_structure
end.should change(SeAccount, :count).by(3)
end
...or some such depending on what your schema looks like, etc. Not sure what exactly is going on in delete_existing_statistics so modify the change clause accordingly.
EDIT: Didn't understand the question at first, my apologies. You could try asserting the following to make sure these calls occur in a given order (again, using RSpec):
EDIT: You can't assert an expectation against a transaction in a test that has expectations for calls within that transaction. The closest I could come up with off the cuff was:
describe StructureUpdater do
before(:each) do
#structure_updater = StructureUpdater.new(Structure.new)
end
it "should update the model within a Transaction" do
SeAccount.should_receive(:transaction)
#structure_updater.update_structure
end
it "should do these other things" do
#structure_updater.should_receive(:delete_existing_statistics).ordered
#structure_updater.should_receive(:delete_existing_structure).ordered
#structure_updater.should_receive(:add_campaigns).ordered
#structure_updater.update_structure
end
end
ONE MORE TRY: Another minor hack would be to force one of the later method calls in the transaction block to raise, and assert that nothing has changed in the DB. For instance, assuming Statistic is a model, and delete_existing_statistics would change the count of Statistic in the DB, you could know that call occurred in a transaction if an exception thrown later in the transaction rolled back that change. Something like:
it "should happen in a transaction" do
#structure_updater.stub!(:add_campaigns).and_raise
lambda {#structure_updater.update_structure}.should_not change(Statistic, :count)
end

Resources