Rails/RSpec - How to write rspec tests for handlers? - ruby-on-rails

I have a handler which is responsible for creating a new record in db.It takes params from the POST requests and adds a new record to database.That helper is defined below:
module Vehicles
module Handlers
class AddVehicle
def initialize(vehicle_params)
#vehicle_info = vehicle_params
end
def call
create_vehicle
end
private
def create_vehicle
Vehicle.create!(#vehicle_info)
end
end
end
end
I would like to test If that class methods work properly but without issuing the POST request.So I don't need to create data with factory_bot but simply puts arguments when calling the method. Do you have an idea how to test this behaviour?

A handler is a class, so just need to do something like this...
h = Vehicle::Handlers::AddVehicle.new(vehicle_params)
to get an instance of the handler. Then you can test as you would any other class. For instance...
vehicle = h.call
refute_nil vehicle

Related

In Ruby on Rails tests, how do I call from a ControllerTest a method defined inside a Controller

I have a test class called AdControllerTest, which I am using to test AdController.
From AdControllerTest, I'm trying to call a method defined in AdController, but I don't think I'm doing it right and I can't find the correct way to do this.
My test code looks like so
test "pctr to final list is correct for pctr policy" do
# Make a CTR list
# Make a selectedAds list
# Check that the CTR list reorders the selectedAds appropriately
response = AdCampaign.search query: {
bool: {
must: [
{ match: { target_gender: "F" },
match: { target_country: "KR" } } ]
}}
selectedAds = Array.new(NUMBEROFADS) {Hash.new}
for i in 1..NUMBEROFADS do
selectedAds[i-1] = response.results.to_a[i-1]
end
testCTR = [0.032521635096847835, 0.03863127908388814, 0.007986670179316374]
finalAds = AdController.pctrToAd(selectedAds: selectedAds, pctr: testCTR)
# Manually order selectedAds by testCTR and compare
comparisonAds = Array.new(NUMBEROFADS) {Hash.new}
comparisonAds[0] = selectedAds[1]
comparisonAds[1] = selectedAds[0]
comparisonAds[2] = selectedAds[2]
assert_equal(finalAds, comparisonAds)
end
And within that code I'm trying to call finalAds = AdController.pctrToAd(selectedAds: selectedAds, pctr: testCTR)
The method pctrToAd is definitely defined in AdController.
But I get an error like so:
Error:
AdControllerTest#test_pctr_to_final_list_is_correct_for_pctr_policy:
NoMethodError: undefined method `pctrToAd' for AdController:Class
test/controllers/ad_controller_test.rb:166:in `block in <class:AdControllerTest>'
Am I not supposed to call the method inside a controller that way? If not, how am I supposed to call it?
AdController.pctrToAd is calling a method on the AdController class. Presumably you want to call a method on an AdController object.
NoMethodError: undefined method `pctrToAd' for AdController:Class
^^^^^^^^^^^^^^^^^^
Assuming this is AdControllerTest, the controller object is available as #controller.
finalAds = #controller.pctrToAd(selectedAds: selectedAds, pctr: testCTR)
You don't.
The only methods inside your controller that should be public are the actions of the controller that are called by the router when it responds to HTTP requests.
Those are tested by sending HTTP requests with integration and system tests to your application.
Writing controller tests is a highly flawed and outdated approach that is not recommended by the Rails core team. Isolating controllers is actually very hard as they are Rack applications and have a hard dependency on an incoming request and the Rack middleware stack. Its also not a good idea as the extensive mocking required lets bugs slip through.
If you have a method that you want to call from the outside it does not belong in the controller. Put it in a model, service object or anywhere else where its actually easy to test it isolation. If you don't need to call/test it directly put it in a private method and test it indirectly.
Controllers have tons of responsibilities already. Don't turn them into the junk drawers of your application.
What you do instead.
Extract the logic of the method that needs to be testable in isolation. This can either be into the model layer or a Plain Old Ruby object such as a service object:
# app/services/ad_campains/frobnobizer_service.rb
module AdCampaigns
class FrobnobizerService
def initialize(selected_ads:, pctr:)
#selected_ads = selected_ads
#pctr = pctr
end
def call
# do something amazing
end
def self.call(selected_ads:, pctr:)
new(...).run
end
end
end
require "test_helper"
class AdCampaignsFrobnobizerServiceTest < Minitest::Test
test "fobnobizes the whatchamacallits" do
result = AdCampaigns::FrobnobizerService.call(
selected_ads: [1,2,3], pctr: 'foobarbaz'
)
assert_equals(result[:foo], 'bar')
end
end
When you compare this with a controller its extremely easy to test as it has very few dependencies and moving parts and only does one job.
You then test that the controller calls this component indirectly by sending a HTTP request:
class AdCampaignsController < ApplicationController
# GET /ad_campigns/frobnobize
def frobnobize
#stuff = AdCampaigns::FrobnobizerService.call(
selected_ads: [1,2,3], pctr: 'foobarbaz'
)
# do something with #stuff
end
end
require "test_helper"
class AdCampaignsFlowTest < ActionDispatch::IntegrationTest
test "GET /ad_campigns/frobnobize" do
get '/ad_campigns/frobnobize'
# test that the controller called the service by
# writing assertions/refutions about the headers
# response body or potential side effects.
end
end
In some cases you might want to use spies/mocks to ensure that the controller calls the collaborator as expected instead of testing the outcome.

How to test non static method was being called by other method using Rspec

I'm trying to test a method being called by another method.
I don't want to test what the other method do, because this is a separate unit test.
so let's say I have something like:
class MyService
def method_a
a = 1
b = method_b
return a + b
end
def method_b
return 2
end
end
Now, I want to test method_a - I want to verify that method_b was executed.
I know that this should work if the methods were static. But in my case, it's not static.
allow(MyService).to receive(:method_b)
I keep getting this error:
MyService does not implement method_b
And I understand that's because the method is not static, but I can't find anything in the documentation that fit my use case.
I think main problem problem is that you expecting for class method to be called and not instance
describe MyService do
it "should call method_b" do
expect(subject).to receive(:method_b).and_return(2)
subject.method_a
end
end
# P.S. it's the same as:
describe MyService do
it "should call method_b" do
service = MyService.new # instead of MyService.new you can also write described_class.new
expect(service).to receive(:method_b).and_return(2)
service.method_a
end
end

Rspec controller test - how to stub and/or test custom objects

Below is passing!
Controller code:
class OrdersController
def create
...
#order.save
end
end
Spec code:
describe OrdersController do
it "should call save method" do
Order.any_instance.should_receive(:save)
post :create
end
end
But if only it were that easy... I have some custom job objects that are executed after the save, so the code actually looks like this:
Controller code:
class OrdersController
def create
...
#order.save
RoadrunnerEmailAlert.new.async.perform(#order.id, true)
CalendarInvite.new.async.perform(#order.id)
RoadrunnerTwilioAlert.new.async.perform(#order.id)
end
end
I would love to test that the custom objects are receiving the chain of methods with the right parameters, but not sure how, short of creating something in the spec code like this:
before do
class RoadrunnerEmailAlert
def async
end
end
end
But that's so contrived, it certainly isn't right... advice appreciated!
In case this helps other people... this is a very comprehensive answer.
Context & design notes
The async framework is Sucker Punch gem
(http://brandonhilkert.com/blog/why-i-wrote-the-sucker-punch-gem/).
Back then, this was the easiest thing for me to use after looking at
Delayed Job, Sidekick, etc
Basically it works like this: in Controller reference a Job that then references anything else (in my case, some POROs)
If I were really rigidly testing, I'd want to test that A) the Controller calls the Job appropriately and passes the right parameters, and B) the Job calls the appropriate POROs and passes the right parameters. But instead, I just tested that the Controller calls the appropriate POROs and passes the right parameters, i.e., the Jobs are already working.
Controller code
#order.save
RoadrunnerEmailAlert.new.async.perform(#order.id, true)
CalendarInvite.new.async.perform(#order.id)
RoadrunnerTwilioAlert.new.async.perform(#order.id)
Job code
# app/jobs/roadrunner_email_alert.rb
class RoadrunnerEmailAlert
include SuckerPunch::Job
def perform(order_id, require_tos)
ActiveRecord::Base.connection_pool.with_connection do
OrderMailer.success_email(order_id, require_tos).deliver
end
end
end
# app/jobs/calendar_invite.rb
class CalendarInvite
include SuckerPunch::Job
def perform(order_id)
ActiveRecord::Base.connection_pool.with_connection do
CreateCalendar.new(order_id).perform
end
end
end
# app/jobs/roadrunner_twilio_alert.rb
class RoadrunnerTwilioAlert
include SuckerPunch::Job
def perform(order_id)
ActiveRecord::Base.connection_pool.with_connection do
CreateAlert.new(order_id).perform
end
end
end
Test code
The really big thing here that I don't know why I keep forgetting (but only in testing) is class vs. instance of class. For the POROs, since I'm instantiating the object, I needed to test 2 different "layers" (first that the object is instantiated appropriately, second that the instantiated object is acted upon appropriately).
require 'sucker_punch/testing/inline'
describe "Controller code" do
before do
OrderMailer.any_instance.stub(:success_email)
mock_calendar = CreateCalendar.new(1)
CreateCalendar.stub(:new).and_return(mock_calendar)
CreateCalendar.any_instance.stub(:perform)
mock_alert = CreateAlert.new(1)
CreateAlert.stub(:new).and_return(mock_alert)
CreateAlert.any_instance.stub(:perform)
end
it "should call appropriate async jobs" do
expect_any_instance_of(OrderMailer).to receive(:success_email).with(1, true)
expect(CreateCalendar).to receive(:new).with(1)
expect_any_instance_of(CreateCalendar).to receive(:perform)
expect(CreateAlert).to receive(:new).with(1)
expect_any_instance_of(CreateAlert).to receive(:perform)
post :create
end
end

How to write RSpec for a controller method not accessible via GET and uses params?

def method1
if params[:hotel].present?
end
end
def method2 //accessed via GET
#hotel = params[:hotel]
method1
end
So, now I want to write a RSpec test for method1. How do I preset the params variable? Just to be clear, I cannot write
get :'method1', request_params
You can do this by literally setting the params yourself and calling the method on the controller eg:
expect(#controller).to receive(:params).and_return(:hotel => "Sample Hotel")
expect(...) # your spec expectations here
expect(#controller.send(:method1)).to eq(expected_return_value)
As mentioned, though, it is considered not best practice to test private/non-action methods of your controller...
Instead you're expected to test the public actions, and test that your private methods are doing the right thing by passing in all the possible variations and getting the final results - eg instead of specifically testing method1, you'd instead test method2 with all the variants that method1 should expect to respond to correctly.

call an class method given an instance in ruby

I'm using rails ActiveModel, I defined 2 methods like this:
def find_existing_task(task)
existing_one = Task.find(task.id)
end
def find_existing_person(person)
existing_one = People.find(person.id)
end
But I think I need a more generic method like this:
def find_existing(any_active_model_instance_with_id)
existing_one = ActiveModelClass.find(any_active_model_instance_with_id.id)
end
But I don't know how to call the class method given an instance, in above, given task, I can call Task.find without specifing class name "Task"
Any solution? thanks!
You can do it with the following code
def find_existing(some_model)
existing_one = some_model.class.find(some_model.id)
end

Resources