Rspec model callback - ruby-on-rails

I have this model that my senior dev wrote:
class Thing < ActiveRecord::Base
after_commit on: :create do
SomeMobule.some_method(self)
end
end
I'm wondering how to test this callback.
I've known from the wise of the internet that you can do this:
(in model)
class Thing < ActiveRecord::Base
after_commit :do_something
def do_something
# doing stuff
end
end
(in spec)
it 'fires do_something after commit' do
expect(#instance).to receive(:do_something)
#instance.save
end
But I have no idea how to deal with this callback block.
Method name can be presented in symbol, easy, but what is another module's method name like in symbol? Or there's some other way to receive?
This might come from my lack of Ruby knowledge or for that matter general programming knowledge, and I have no idea even how to pursue the answer on the internet.

You can just test that SomeModule.some_method(self) is called.
let(:thing) { Thing.new }
it 'calls SomeModule.do_something after commit' do
expect(SomeModule).to receive(:do_something).with(thing)
thing.save
end
Which is fine if SomeModule.do_something is an application boundary such as a client to an external API.
If its not the test is very low value from a BDD standpoint - it only tests how the pieces are glued together - not the actually behaviour. A better test would be to test that the expected behaviour is triggered when you save the model.
# a really contrived example
it 'becomes magical when it is saved' do
expect do
thing.save
thing.reload
end.to change(thing, :magical).from(false).to(true)
end

Related

Globally mock method calling external API

Background: I'm bringing to life a 6-year-old Rails project and haven't touched the framework since then. Thus, I'm re-learning many things.
I'm trying to understand the best approach to mock an API call that needs to be done synchronously. An Order has_one Invoice, and Invoice must get a reference from an external service. An Order is useless without an Invoice.
Below is a simple version of the application. The Order model is core to the application.
Open questions:
Is the best practise to globally mock SDKs in spec_helper.rb? Which would contain my allow_any_instance_of(InvoiceServiceSdk)
I have an Order factory, used almost everywhere in my tests. But I'm confused if I can loop in an Invoice factory as well. FactoryBot feels quite alien to me at the moment.
# app/models/order.rb
class Order < ApplicationRecord
has_one :invoice, autosave: true
before_create :build_invoice
def build_invoice
self.invoice = Invoice.new
end
end
# app/models/invoice.rb
class Invoice < ApplicationRecord
belongs_to :order
before_create :generate
def generate
invoice_service = InvoiceServiceSdk.new
self.external_id = invoice_service.fetch
end
end
# app/models/invoice_service_sdk.rb
require 'uri'
require 'net/http'
class InvoiceServiceSdk
def fetch
uri = URI('https://example.com/') # Real HTTP request
res = Net::HTTP.get_response(uri)
SecureRandom.urlsafe_base64 # "ID" that API "provides"
end
end
# spec/models/order.rb
require 'rails_helper'
RSpec.describe Order, type: :model do
before do
allow_any_instance_of(InvoiceServiceSdk).to receive(:fetch).and_return('super random external invoice ID')
end
context "new order + invoice" do
it {
o = Order.new
o.save
expect(o.invoice.external_id).to eq 'super random external invoice ID'
}
end
end
The rspec-mocks documentation discourages the use of allow_any_instance:
The rspec-mocks API is designed for individual object instances, but this feature operates on entire classes of objects. As a result there are some semantically confusing edge cases. For example, in expect_any_instance_of(Widget).to receive(:name).twice it isn't clear whether a specific instance is expected to receive name twice, or if two receives total are expected. (It's the former.)
Using this feature is often a design smell. It may be that your test is trying to do too much or that the object under test is too complex.
You can avoid it completely by just adding a factory method to your service objects:
class MyService
def intialize(**kwargs)
#options = kwargs
end
def call
do_something_awesome(#options[:foo])
end
def self.call(**kwargs)
new(**kwargs).call
end
end
allow(MyService).to recieve(:call).and_return([:foo, :bar, :baz])
Is it smelly to Stub the request in spec_helper?
Not necissarily. You can avoid a bit of overhead by refactoring the code as indicated above and stubbing the factory method. It also makes it so that your stubs are not coupled to the inner workings of the service object.
I would be more worried about the fact that this code does one thing right by using a service object and then immediately cancels that out by calling it in a model callback.

Testing class which uses refinements with RSpec

Let's say I've got refinement
module RefinedString
refine String do
def remove_latin_letters
#code code code code
end
end
end
and I use it inside my class Speech:
class Speech
using RefinedString
def initialize(text)
#content = text.remove_latin_letters
end
end
I've written tests for refinement in RSpec and now I'm testing Speech class
describe Speech
let(:text) { "ąńńóyińg" }
it 'should call my refinement' do
expect(text).to receive(:remove_latin_letters)
Speech.new(text)
end
end
but I get RSpec::Mocks::MockExpectationError: "ąńńóyińg" does not implement: remove_latin_letter
I don't think mocking it is a good solution (but I may be wrong! Is mocking the solution here?)
so I tried
let(:text) { described_class::String.new("ąńńóyińg") }
but the result is the same.
I don't want to explicitly call using RefinedString inside my RSpec (it should figure it out on its own, right?)
How to make RSpec aware of my refined methods?
We always want to test behavior, rather than implementation. To my mind, refinements change the behavior of other classes by virtue of being included, rather than having their own behavior. To use a somewhat clumsy analogy, if we were to test the reproductive behavior of a virus, we would have to introduce it into a host cell. We are interested in what happens to the host when the virus takes over (so to speak).
One approach is to build test classes with and without the refinement, e.g.:
class TestClass
attr_reader :content
def initialize(text)
#content = text.remove_latin_letters
end
end
describe "when not using RefinedString" do
it "raises an exception" do
expect { TestClass.new("ąńńóyińg") }.to raise_error(NoMethodError)
end
end
class RefinedTestClass
using RefinedString
attr_reader :content
def initialize(text)
#content = text.remove_latin_letters
end
end
describe "when using RefinedString" do
it "removes latin letters" do
expect(RefinedTestClass.new("ąńńóyińg").content).to eq "ńńóń"
end
end

Organizing API-Calls in callbacks

We are maintaining several Rails-Apps which all pose a similar problem that we don't have a really good solution to: All these apps contain models that need to make a API-Call to an external service in their lifecycle.
Possible cases:
User is subscribed to a Newsletter-subscriber-list, when successfully created
Prices for an offer are synced with an external shopping-system after updating
Product is updated in the Search-Index after updating
What we exprienced to NOT be a good solution: Adding these calls to the after_*callbacks of the model. Since that breaks tests fast, cause all factories now have to deal with the api-calls.
I'm looking for a good way to organize these API-call. How do you guys do this?
Ideas we came up with, which I considered not real ideal:
Moving those callbacks to the controller. Now they get easily forgotten, when creating an object
Spawning an asynchronous worker to handle the api-call. Then every - even small app - needs to have the overhead of a delayed job-queue, like sidekiq.
If you are concerned about testing you could put the callback methods into a separate class and mock the callback class during testing. Here's an example using RSpec, given the following Foo and FooCallbacks classes:
class Foo < ActiveRecord::Base
after_save FooCallbacks
end
class FooCallbacks
def self.after_save
fail "Call to external API"
end
end
You can write and successfully run a spec like this:
describe Foo do
before do
allow(FooCallbacks).to receive(:after_save)
end
it "should not invoke real APIs" do
Foo.create
end
end
This is how I now did it, after the advise:
In Foo:
class Foo < ActiveRecord::Base
before_save Foo::DataSync
end
Foo:DataSynclooks like this:
class Foo::DataSync
def self.before_save(foo)
...do the API-Calls...
end
end
Now for testing in rspec I added this:
To spec_helper.rb:
config.before(:each) do
Foo::DataSync.stub(:before_save)
end
Note that config.before(:suite) will not work, since Foo:DataSync is not loaded at that time.
Now foo_spec.rb contains just this:
describe Foo do
let(:foo) {create(:foo)}
it "will sync its data before every save" do
expect(Foo::DataSync).to receive(:before_save).with(foo)
foo.save
end
end
The Foo::DataSync can be tested like this:
describe Foo::DataSync do
let!(:foo) {create(:foo)}
before do
Foo::DataSync.unstub(:before_save)
end
after do
Foo::DataSync.stub(:before_save)
end
describe "#before_save" do
...my examples...
end
end

How to disable belongs_to :touch option in Rspec tests for Rails models?

Having a large model stack and using doll caching techniques extensively, one ends up with lots of parent models been "touched" after a model update.
While testing, this seems to be a time waster unless you try to test that feature specifically.
Is there a way to prevent models to touch their belongs_to associations for the test environment or at a test level?
UPDATE 1:
My first attempt to the case would be to
# /config/initializers/extensions.rb
#
class ActiveRecord::Base
def self.without_touch_for_association(association_name, &block)
association_name = association_name.to_sym
association = self.reflect_on_all_associations(:belongs_to).select { |reflection| reflection.name == association_name }.first
options = association.options
association.instance_variable_set :#options, options.except(:touch)
yield
association.instance_variable_set :#options, options
end
end
Post.without_touch_for_association(:user) do
Post.last.save
end
Of course, no success and saving Post.last still touches it's User.
UPDATING RATIONALE:
I understand and agree that this approach may be a source of bugs and it's not a good practice at all. The thing is that I have a huge suite with lots of both integration and unit tests. Doll caching also gets deep in the model tree. Every time I look at the logs, I see a significant % of touch-related queries. I know the best way would be optimizing the unit tests to add more mocking and stubbing and less persistence. Solving the issue within integration tests is more difficult.
In any case, I'm asking this question for the sake of learning and research. I am interested in exploring the potential speed improvements of this technique.
SOLUTION: see my own answer below for the working code.
Assuming you're on Rails 4.1.4 or newer:
User.no_touching do
Post.last.save
end
or even
ActiveRecord::Base.no_touching do
Post.last.save
end
See ActiveRecord::NoTouching.
I disagree with the notion of altering the code for test purposes. Testing from my point of view should be an independent procedure.
As I see it you should provide a way to your test suite to alter the behavior of a model only for certain cases.
The following code
class Book < ActiveRecord::Base
belongs_to :author, touch: true
end
class Author < ActiveRecord::Base
has_many :books
end
which is your case will define an instance method
belongs_to_touch_after_save_or_destroy_for_author
behind the scene for the Book class.
( thanks to AR http://apidock.com/rails/ActiveRecord/Associations/Builder/BelongsTo/add_touch_callbacks )
So in your test code you could override that method to do something different or nothing at all!
In my case I use Rspec with FactoryGirl, so what I did was to create a special factory trait for the Book class which redefines belongs_to_touch_after_save_or_destroy_for_author for that object
FactoryGirl.define do
factory :book do
...
...
end
trait :no_touch do
before(:create) do |book_no_touch|
def book_no_touch.belongs_to_touch_after_save_or_destroy_for_author
true
end
end
end
end
That way, when you need to test something where touching the related objects is irrelevant, you can create a book object with that factory
book = FactoryGirl.create(:book, :no_touch)
For Rails >= 4.2
Thanks to #Dorian, in Rails 4.2 the way to go is using ActiveRecord::NoTouching.
For Rails < 4.2
My working code in rspec support file:
# /spec/support/active_record_extensions.rb
class ActiveRecord::Base
def self.without_touch_for_association(association, &block)
method_name = :"belongs_to_touch_after_save_or_destroy_for_#{association}"
return unless self.instance_methods.include?(method_name)
method = self.send(:instance_method, method_name)
self.send(:define_method, method_name) { true }
yield
self.send(:define_method, method_name, method)
nil
end
def self.disable_touch_associations!
associations = self.reflect_on_all_associations(:belongs_to)
associations.each do |association|
self.without_touch_for_association association.name do
return
end
end
nil
end
end
Add this to your ./spec/spec_helper.rb to disable all touch calls for any model defined, for the whole test suite:
RSpec.configure do |config|
if ENV['SILENCE_TOUCHES']
config.before :suite do
ActiveRecord::Base.descendants.each {|model| model.disable_touch_associations! }
end
end
end
Temporarely disabling a touch for a model and association in a particular test.
Post.without_touch_for_association(:user) do
Post.last.save
end
Thanks to #xlembouras below for pointing me to the right direction!
I'm playing with this feature on our tests and I'm noticing a 25% reduction in test suite speed, for a 30min test suite. I may post more accurate results after more thorough research.
I'm not sure if this is going to work but you could try the following:
belongs_to :foo, touch: APP_CONFIG['doll_touch']
where APP_CONFIG is an application parameter that is set following this guide.
So, in your production/development part of the configuration, you set doll_touch to true and in your test to false.

How would you test observers with rSpec in a Ruby on Rails application?

Suppose you have an ActiveRecord::Observer in one of your Ruby on Rails applications - how do you test this observer with rSpec?
You are on the right track, but I have run into a number of frustrating unexpected message errors when using rSpec, observers, and mock objects. When I am spec testing my model, I don't want to have to handle observer behavior in my message expectations.
In your example, there isn't a really good way to spec "set_status" on the model without knowledge of what the observer is going to do to it.
Therefore, I like to use the "No Peeping Toms" plugin. Given your code above and using the No Peeping Toms plugin, I would spec the model like this:
describe Person do
it "should set status correctly" do
#p = Person.new(:status => "foo")
#p.set_status("bar")
#p.save
#p.status.should eql("bar")
end
end
You can spec your model code without having to worry that there is an observer out there that is going to come in and clobber your value. You'd spec that separately in the person_observer_spec like this:
describe PersonObserver do
it "should clobber the status field" do
#p = mock_model(Person, :status => "foo")
#obs = PersonObserver.instance
#p.should_receive(:set_status).with("aha!")
#obs.after_save
end
end
If you REALLY REALLY want to test the coupled Model and Observer class, you can do it like this:
describe Person do
it "should register a status change with the person observer turned on" do
Person.with_observers(:person_observer) do
lambda { #p = Person.new; #p.save }.should change(#p, :status).to("aha!)
end
end
end
99% of the time, I'd rather spec test with the observers turned off. It's just easier that way.
Disclaimer: I've never actually done this on a production site, but it looks like a reasonable way would be to use mock objects, should_receive and friends, and invoke methods on the observer directly
Given the following model and observer:
class Person < ActiveRecord::Base
def set_status( new_status )
# do whatever
end
end
class PersonObserver < ActiveRecord::Observer
def after_save(person)
person.set_status("aha!")
end
end
I would write a spec like this (I ran it, and it passes)
describe PersonObserver do
before :each do
#person = stub_model(Person)
#observer = PersonObserver.instance
end
it "should invoke after_save on the observed object" do
#person.should_receive(:set_status).with("aha!")
#observer.after_save(#person)
end
end
no_peeping_toms is now a gem and can be found here: https://github.com/patmaddox/no-peeping-toms
If you want to test that the observer observes the correct model and receives the notification as expected, here is an example using RR.
your_model.rb:
class YourModel < ActiveRecord::Base
...
end
your_model_observer.rb:
class YourModelObserver < ActiveRecord::Observer
def after_create
...
end
def custom_notification
...
end
end
your_model_observer_spec.rb:
before do
#observer = YourModelObserver.instance
#model = YourModel.new
end
it "acts on the after_create notification"
mock(#observer).after_create(#model)
#model.save!
end
it "acts on the custom notification"
mock(#observer).custom_notification(#model)
#model.send(:notify, :custom_notification)
end

Resources