Globally mock method calling external API - ruby-on-rails

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.

Related

Rspec model callback

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

what is the 'Rails Way' to pass objects across multi (logical) tiers?

I'm coming from the .NET world and I'm trying to figure out what the 'Rails Way' to pass an object across tiers in a multi-tier application.
I'm writing a multi carrier pricing API. Basically in my price controller I have access to the following parameters params[:carrier], params[:address_from], params[:address_to], params[:container_type], etc. I have a validation library, a compliance library and a price-finder library that each deal with a subset of the params.
In .NET the params would be encapuslated in data transfer objects (DTOs) or contracts. Before calling any of the libraries, they would be converted to domain objects (DOs) and each library would work on the DOs, thus avoiding a tight coupling on the DTOs. Ruby programming recommands the use of 'duck typing', so my libraries could work directly on params (even though you would access symbols and not objects/properties). Or should I marshall my params into a PriceRequest object and have my libraries work on the PriceRequest type?
Option 1:
class PricesController < ApplicationController
def get
CarrierValidator.validate(params)
...
end
end
class CarrierValidator
def self.validate(params)
raise CarrierError if !Carrier.find_by_name(params[:carrier_name]).exists?
end
end
Option 2:
class PricesController < ApplicationController
def get
pricesRequest = PricesRequest.new(carrier_name: params[:carrier_name], ...)
pricesRequest.validate
...
end
end
class PriceRequest
attr_accessor : ...
def initalize
...
end
def validate
CarrierValidator.validate(self.carrier_name)
end
end
class CarrierValidator
def self.validate(carrier_name)
raise CarrierError if !Carrier.find_by_name(carrier_name).exists?
end
end
TIA,
J
You should create a type. I would use ActiveModel to encapsulate the data (attributes) & business logic (validations & maybe some layer-specific methods for processing the data).
Basically, you want to be able to do Rails-y things in the controller like:
def get
price_request = PriceRequest.new(params[:price_request])
if price_request.valid?
# do something like redirect or render
else
# do something else
end
end
so you want to declare:
class PriceRequest
include ActiveModel::Model
attr_accessor :carrier, :address_from, :address_to, :container_type
validates :carrier, presence: true
validate :validate_address_from
def validate_address_from
# do something with errors.add
end
# and so on
This is a good place to start: http://edgeguides.rubyonrails.org/active_model_basics.html
More details in the API: http://api.rubyonrails.org/classes/ActiveModel/Model.html
Hope that points you in the right direction...

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

Testing dynamically created methods

In my model, I dynamically create some methods based on database records:
class Job < ActiveRecord::Base
belongs_to :job_status
# Adds #requisition?, #open?, #paused?, #closed?
class_eval do
JobStatus.all.each do |status|
unless method_defined? "#{status.name.downcase}?"
define_method("#{status.name.downcase}?") do
job_status_id == status.id
end
end
end
end
end
class JobStatus < ActiveRecord::Base
has_many :jobs
end
The job_statuses table contains some seed data, so is not going to be frequently changing, but in case I ever need to add new statuses, I don't have to add more code to get a boolean method for the new status.
However, I am not sure how to test these methods, because when rspec starts the job_statuses table is obviously empty, and when the JobStatus objects are created, Job gets initialized, but since no objects exist yet, it doesn't create any methods, and my tests fail because the methods don't exist.
Note that I am using rspec with spork & guard, and using database-cleaner with the truncation strategy (as per Railscast #257, since I'm using Selenium), so that probably complicates matters.
The solution I came up with was to abstract the creation of runtime methods out into a library file, and then in my test file, remove and redeclare my class before each test, and reload the actual class (and blueprints) at the end of the suite:
describe AssociationPredicate do
before(:all) do
["Continuous", "Standard"].each { |type| JobType.create!(:job_type => type) }
["Requisition", "Open", "Paused", "Closed"].each { |status| JobStatus.create!(:job_status => status) }
end
after(:all) do
DatabaseCleaner.clean_with :truncation, :only => %w( job_types job_statuses )
# Reload Job model to remove changes
Object.send(:remove_const, 'Job')
load 'job.rb'
load 'support/blueprints.rb'
end
before(:each) do
Object.send(:remove_const, 'Job')
# Redefine Job model for testing purposes
class Job < ActiveRecord::Base
belongs_to :job_type
belongs_to :job_status
has_many :job_applications
end
end
it "should add methods when included" do
Job.send(:association_predicate, :job_type)
job.should respond_to(:continuous?)
job.should respond_to(:standard?)
end
end
This way, I create a basic class for each test, add the runtime methods as necessarily, and return to the actual class when I'm done.
Try with enumerize gem. This make your status field like enumerator and build the "#{status.name.downcase}?" for your models. This gem came with it's own rspec-matchers making easiest your unit test.

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