Rspec controller spec claims function ios not called - ruby-on-rails

I am testing out one of my controllers and have attempted to stub a function call with no luck. Here is the function:
def fetch_typeform_response
hp = HealthProfile.find(params[:id])
form = TypeformService.new('x')
response = form.getResponse("query=#{ hp[:id] }")
if response['total_items'] != 1
if response[:response_id].present?
response = form.getResponse("included_response_ids=#{ hp[:response_id] }")
end
end
if response['total_items'] == 1
response = response['items'].first
health_profile = HealthProfile.map_typeform_response(response)
if health_profile.save
health_profile.reload
redirect_to health_profile_path(health_profile), notice: "Successfully updated the health profile response."
return
end
end
redirect_to health_profiles_path, notice: "We could not locate the health profile."
end
In my test, I stub out :getResponse and :map_typeform_response since they involve an outside API:
it "expects to fetch typeform response" do
new_hp = build(:health_profile)
new_hp_after_mapping = build(:health_profile)
allow_any_instance_of(TypeformService).to receive(:getResponse).and_return({ 'total_items': 1, 'items': [ new_hp ] }.as_json)
allow_any_instance_of(HealthProfile).to receive(:map_typeform_response).and_return(new_hp_after_mapping)
get :fetch_typeform_response, params: { id: #hp.id }
expect(response).to redirect_to(health_profile_path(#hp.id))
end
But I receive the error: HealthProfile does not implement #map_typeform_response.
If I remove the stub line, I see the error:
Failure/Error: p "Using health_profile_id: #{response['hidden']['id']}"
NoMethodError:
undefined method `[]' for nil:NilClass
Which is occurring inside the :map_typeform_response function (so clearly it is called!). Any idea why this might happen?

You are calling map_typeform_response method on class HealthProfile and not on instance of the class.
change
allow_any_instance_of(HealthProfile).to receive(:map_typeform_response).and_return(new_hp_after_mapping)
to
allow(HealthProfile).to receive(:map_typeform_response).and_return(new_hp_after_mapping)
That happens because rspec prevents you from mocking or stubbing a method that does not exist on a real object. Default is true since Rails 4.
RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
Some more recommendations
I'd also recommend to move building of new_hp and new_hp_after_mapping variables to let
let(:new_hp) { build(:health_profile) }
let(:new_hp_after_mapping) { build(:health_profile) }
move stubs to before
before do
allow_any_instance_of(TypeformService).to receive(:getResponse).and_return({ 'total_items': 1, 'items': [ new_hp ] }.as_json)
allow(HealthProfile).to receive(:map_typeform_response).and_return(new_hp_after_mapping)
end
so your test will look like
it "expects to fetch typeform response" do
# make sure variable #hp intialized in your test.
get :fetch_typeform_response, params: { id: #hp.id }
expect(response).to redirect_to(health_profile_path(#hp.id))
end

Related

Rails Rspec - How to test if Service has been called in another Service

While writing tests, I stopped at trying to test Service in another Service. In such a situation, I should probably just check if Service has been called because it has already been tested elsewhere. I did a little research on the Internet and found something like have_received but I have no idea how to use it in my example.
check_service.rb
Class CheckService
def initialize(params)
#params = params
end
def self.call(params)
new(params).call
end
def call
CheckUser.call(params[:user_id])
end
end
check_service_spec.rb
...
describe 'call' do
let(:result) { CheckService.call(params) }
let(:params) { { user_id: "100" } }
let(:check_user) { instance_double(CheckUser) }
before do
allow(check_user).to receive(:call).and_return(true)
end
it do
result
expect(check_user).to have_received(:call)
end
end
...
I was trying something like this (it's simple example), but I get error:
(InstanceDouble(CheckUser) (anonymous)).call(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Is there any option to test situation I presented?
Short anwser
describe 'call' do
let(:result) { CheckService.call(params) }
let(:params) { { user_id: "100" } }
## let(:check_user) { instance_double(CheckUser) } delete this
before do
allow(CheckUser).to receive(:call).and_return(true)
end
it do
result
expect(CheckUser).to have_received(:call)
end
end
Alternative
I think a better way to test this is to use DI (Dependency Injection), so you pass CheckUser as a dependency to CheckService. I prefer to write the whole test inside the it block too!
class CheckService
def initialize(params, check_handler:)
#params = params
#check_handler = check_handler
end
def self.call(params, check_handler: CheckUser)
new(params, check_handler: check_handler).call
end
def call
#check_handler.call(#params[:user_id])
end
end
describe 'call' do
it 'check user with params' do
check_user = class_double(CheckUser)
allow(check_user).to receive(:call).and_return(true)
params = { user_id: "100" }
CheckService.call(params, check_handler: check_user)
expect(check_user).to have_received(:call)
end
end
A blog post to read more about -> https://blog.testdouble.com/posts/2018-05-17-do-we-need-dependency-injection-in-ruby/

Rails test that method is called from the controller

I have a controller function that calls a service and I want to test that the service is called with the right arguments.
def send_for_signature
client = Client.find(params[:client_id])
external_documents = params[:document_ids].map{|id| ExternalDocument.find(id)}
service = EsignGenieSendByTemplate.new(client: client, external_documents: external_documents, form_values: params[:form_values])
result = service.process
if result["result"] == "success"
head 200
else
render json: result["error_description"], status: :unprocessable_entity
end
end
How would I write my test to ensure that EsignGenieSendByTemplate.new(client: client, external_documents: external_documents, form_values: params[:form_values]) is called correctly?
I would start by adding a factory method to the service:
class EsignGenieSendByTemplate
# ...
def self.process(**kwargs)
new(**kwargs).process
end
end
This kind of code is boilerplate in almost any kind of service object and provides a better API between the service object and its consumers (like the controller).
This method should be covered by an example in your service spec.
describe '.process' do
let(:options) do
{ client: 'A', external_documents: 'B', form_values: 'C' }
end
it "forwards its arguments" do
expect(described_class).to recieve(:new).with(**options)
EsignGenieSendByTemplate.process(**options)
end
it "calls process on the instance" do
dbl = instance_double('EsignGenieSendByTemplate')
allow(described_class).to recieve(:new).and_return(dbl)
expect(dbl).to recieve(:process)
EsignGenieSendByTemplate.process(**options)
end
end
Your controller should just call the factory method instead of instanciating EsignGenieSendByTemplate:
def send_for_signature
client = Client.find(params[:client_id])
# Just pass an array to .find instead of looping - this create a single
# db query instead of n+1
external_documents = ExternalDocument.find(params[:document_ids])
result = EsignGenieSendByTemplate.process(
client: client,
external_documents: external_documents,
form_values: params[:form_values]
)
if result["result"] == "success"
head 200
else
render json: result["error_description"], status: :unprocessable_entity
end
end
This better API between the controller and service lets you set an expectation on the EsignGenieSendByTemplate class instead so you don't have to monkey around with expect_any_instance or stubbing the .new method.
it 'requests the signature' do
expect(EsignGenieSendByTemplate).to receive(:process).with(client: 'A', external_documents: 'B', form_values: 'C')
get :send_for_signature, params: { ... }
end
What you need is something called expecting messages.
I usually write something like this:
it 'requests the signature' do
expect(EsignGenieSendByTemplate).to receive(:new).with(client: 'A', external_documents: 'B', form_values: 'C')
get :send_for_signature, params: { ... }
expect(response.status).to have_http_status(:success)
end

how to mock method retry and variable assignment inside method in rspec

require 'requiredclass'
class Test
def get_client()
return some_client
end
def intermediate_method()
res = nil
self.class
.get_client
.retry(tries:5, on: [RequiredClass::ClientTimeout]) do |myclient|
call_count += 1
res = myclient.dosomething()
end
return res
end
def method_to_test()
x = intermediate_method()
y = false
return x && y
end
end
How can I write rspec for method_to_test here. How can I mock get_client.retry as well as calls to get_client while also mocking res variable assignment so that gets assigned the value i would like it to assign.
As written, this code is difficult to test. That's a smell and a sign that the code should be restructured. Really any time you feel tempted to mock a method in the current class, that's a sign that the thing you want to mock does not belong in that class. It should be injected (passed in) instead. Like this:
require 'requiredclass'
class Test
attr_reader :client
def initialize(client)
#client = client
end
def method_to_test
x = intermediate_method
y = false
x && y
end
def intermediate_method
res = nil
client.retry(tries: 5, on: [RequiredClass:ClientTimeout]) do |my_client|
call_count += 1
res = my_client.do_something
end
res
end
end
Given this refactored code, the tests might look like this:
RSpec.describe Test do
subject(:test) { Test.new(client) }
let(:client) { instance_double(Client, retry: true, do_something: true) }
describe '#method_to_test'
subject(:method_to_test) { test.method_to_test }
it 'returns false' do
expect(method_to_test).to be_false
end
end
end
In this code I've passed a double with a stubbed retry method into the Test class on instantiation. You could optionally use a mock, instead. That would look like this:
RSpec.describe Test do
subject(:test) { Test.new(client) }
let(:client) { instance_double(Client) }
before do
allow(client).to receive(:retry)
allow(client).to receive(:do_something)
end
describe '#method_to_test'
subject(:method_to_test) { test.method_to_test }
it 'returns false' do
expect(method_to_test).to be_false
end
end
end
There's a good write up of mocks and doubles in the RSpec documentation.

Rspec: Mock recaptcha verification

I am trying to create a request spec for a form submission and my recaptcha verification is causing the test to fail. I have a pretty simple test:
RSpec.describe "PotentialClients", type: :request do
let(:pc_attributes) { ... }
describe "POST /potential_clients" do
it "should create record" do
expect { post potential_clients_path, params: { potential_client: pc_attributes } }
.to change(PotentialClient, :count).by(+1)
end
end
end
I run into an issue because in PotentialClients#create I make a call to verify_recaptcha? which returns false in the test instead of true:
# potential_clients_controller.rb
def create
#potential_client = PotentialClient.new(potential_client_params)
page_success = verify_recaptcha?(params[:recaptcha_token], 'lead_creation_page')
if page_success && #potential_client.save
...
end
end
# application_controller.rb
def verify_recaptcha?(token, recaptcha_action)
secret_key = ENV['CAPTCHA_SECRET_KEY']
uri = URI.parse("https://www.google.com/recaptcha/api/siteverify?secret=#{secret_key}&response=#{token}")
response = Net::HTTP.get_response(uri)
json = JSON.parse(response.body)
if json['success'] && json['score'] > RECAPTCHA_MINIMUM_SCORE && (json['action'] == "lead_creation_page" || json['action'] == "lead_creation_modal")
return true
elsif json['success'] == false && json["error-codes"].include?("timeout-or-duplicate")
return true
end
return false
end
How should I mock the call to verify_recapthca? so that my test passes? I've tried:
allow(PotentialClient).to receive(:verify_recaptcha?).and_return(true)
# and
allow_any_instance_of(PotentialClient).to receive(:verify_recaptcha?).and_return(true)
but both threw errors:
PotentialClient(...) does not implement: verify_recaptcha?
allow(PotentialClient).to receive(:verify_recaptcha?).and_return(true)
This isn't working because—as the error message says—PotentialClient (the model) doesn't have a method called verify_recaptcha?. The method is defined in ApplicationController, which is extended by PotentialClientsController, and that's where you need to mock it.
My Rails is rusty, but it looks like in an rspec-rails controller spec the current instance of the controller is exposed by the controller method. In that case, what you want is this:
allow_any_instance_of(ApplicationController).to receive(:verify_recaptcha?).and_return(true)

How to know the flow of the controller method using Rspec

I have two dependent drop down.One gives me orgname and other drop down populates on selecting a orgname, That is teamname.
This is my github_leader_board_spec.rb
describe "github_leader_board" do
before do
#obj = DashboardsController.new
end
context "with session" do
subject { get :github_leader_board, :params => { :orgname => "test", :teamname=> "team"}}
it "returns http success" do
expect(response).to have_http_status(:success)
end
it "executes other functions" do
expect(#org_data).not_to be_nil
expect(#obj.get_team_api("DevCenter")).not_to be_nil
end
end
end
This is my controller method
def github_leader_board
myhash = {}
#points_hash = {}
member_data = []
#org_data = get_org_api
#orgs = get_names(org_data)
team_data = get_team_api(params[:orgname])
#teams = get_names(team_data)
teamid = get_team_id(team_data)
#teams.each_with_index {|k,i|myhash[k] = teamid[i]}
myhash.each do |key,value|
if key == params[:teamname]
member_data = get_members("#{value}")
end
end
#memberids = get_names(member_data)
member_names = get_member_names(#memberids)
review_comments = get_reviewcoments(#memberids)
reactions = points(#memberids)
points = [review_comments, reactions].transpose.map {|x| x.reduce(:+)}
member_names.each_with_index {|k,i|#points_hash[k] = points[i]}
end
If i run my spec file it says, undefined #org_data. The function inside the github_leader_board controller is not calling the get_org_api and storing the value to the #org_data variable.
Can anybody suggest what is wrong with the code and how can i improve it. As i'm new to ror.
Any help would be appreciated.
Thank you.
I believe you could use a test of the type controller, instead of instantiating your controller and then use the RSpec method assigns (docs) to test your instance variables, something like this:
RSpec.describe DashboardsController, :type => :controller do
context "with session" do
# ...
it "executes other functions" do
expect(assigns(:org_data)).not_to be_nil
end
end
end
https://relishapp.com/rspec/rspec-rails/docs/controller-specs
Also, if you want to check the flow, and debug your code, you can use the gems pry, pry-rails and pry-nav as #Marek Lipka stated.

Resources