Rails + Rspec: Test that service is called - ruby-on-rails

I am trying to write a test to ensure that my service, WeeklyReportCardService, is instantiated and that it's method :send_weekly_report_card_for_repositioning is called.
Here's the controller:
def update
Audited.audit_class.as_user($user) do
if #check_in.update(check_in_params)
client = Client.find_by(id: check_in_params[:client_id])
if #check_in.repositioning.present? && #check_in.weigh_in.present? && #check_in.client&.location&.name == "World Wide"
# I see this in the console so the if statement returns true
p "hitting send!!"
WeeklyReportCardService.new.send_weekly_report_card_for_repositioning(#check_in.repositioning)
end
render json: #check_in, status: :ok, serializer: API::CheckInsIndexSerializer
else
render json: #check_in.errors.full_messages, sattus: :unprocessable_entity
end
end
end
Here's my test:
RSpec.describe API::CheckInsController, type: :request do
fit "should send if the client's location is World Wide" do
program = create(:program, :with_client)
worldwide = create(:location, name: "World Wide")
program.client.update(location_id: worldwide.id)
check_in = create(:check_in, client_id: program.client.id, program_id: program.id)
create(:repositioning, check_in_id: check_in.id)
create(:weigh_in, check_in_id: check_in.id)
url = root_url[0..-2] + api_check_in_path(check_in.id) + "?sendReportCardEmail=true"
put url, params: { check_in: {type_of_weighin: 'standard'}}, headers: { "HTTP_AUTHENTICATION": #token }
expect_any_instance_of(WeeklyReportCardService).to receive(:send_weekly_report_card_for_repositioning)
end
end
and the error I see is:
Failure/Error: DEFAULT_FAILURE_NOTIFIER = lambda { |failure, _opts| raise failure }
Exactly one instance should have received the following message(s) but didn't: send_weekly_report_card_for_repositioning
What else do I need to do to ensure that function is called?

You're doing it in the wrong order. You need to set the expectation first before the method is expected to be called:
expect_any_instance_of(WeeklyReportCardService).to receive(:send_weekly_report_card_for_repositioning)
put url, params: { check_in: {type_of_weighin: 'standard'}}, headers: { "HTTP_AUTHENTICATION": #token }
If you need to set the expecations afterwards you need to replace the method or object with a spy which is useful if you prefer the arrange-act-assert (or given-when-then)
pattern for structuring tests.
You should also note that the use of any instance is strongly discouraged and you can avoid it by providing a simple class method:
class WeeklyReportCardService
def self.send_weekly_report_card_for_repositioning(...)
new.send_weekly_report_card_for_repositioning(...)
end
end
RSpec.describe API::CheckInsController, type: :request do
it "should send if the client's location is World Wide" do
expect(WeeklyReportCardService).to receive(:send_weekly_report_card_for_repositioning)
put url, params: { check_in: {type_of_weighin: 'standard'}}, headers: { "HTTP_AUTHENTICATION": #token }
end
end
Or alternatively by stubbing the WeeklyReportCardService#new method to return a mock or spy.

Related

Rspec ignore allow block?

I want to test a rails job which call an endpoint of hubspot API (GET /crm/v3/owners/{ownerId}) and update a record with infos of the request result.
The problem is that I use this gem as an API wrapper and my before block seems like ignored because the result shows that the API call can't give me an owner object with this owner_id (the one given as parameter of attributes is obviously fake). A before block is supposed to override the "normal" response of the controller isn't it ?
I really don't know what I am doing wrong ..
For more context:
My job code
module Hubspots
module Contracts
class UpdateJob < BaseJob
queue_as :high_priority
def perform(attributes)
contract = Contract.find_by(hubspot_sales_deal_id: attributes[:hubspot_sales_deal_id])
return if contract.nil?
deal_owner = client.crm.owners.owners_api.get_by_id(owner_id: attributes[:hubspot_tailor_deal_owner],
id_property: 'id', archived: false)
attributes[:hubspot_tailor_deal_owner] = get_owner_name(deal_owner)
contract.update!(attributes)
end
private
def get_owner_name(hubspot_owner_object)
"#{hubspot_owner_object.last_name.upcase} #{hubspot_owner_object.first_name.capitalize}"
end
end
end
end
My test code
RSpec.describe Hubspots::Contracts::UpdateJob, type: :job do
let!(:job) { described_class.new }
let(:perform) { job.perform(attributes) }
let!(:contract) { create(:contract, hubspot_sales_deal_id: 123) }
let!(:attributes) do
{ hubspot_tailor_deal_id: 456, hubspot_tailor_deal_owner: 876, hubspot_sales_deal_id: 123 }
end
let!(:deal_owner_api) { Hubspot::Client.new(access_token: ENV['HUBSPOT_ACCESS_TOKEN']).crm.owners.owners_api }
let!(:deal_owner_properties) { { last_name: 'Doe', first_name: 'John' } }
before do
allow(deal_owner_api).to receive(:get_by_id).and_return(deal_owner_properties)
end
describe '#perform' do
it 'updates contract' do
expect { perform }.to change { contract.reload.hubspot_tailor_deal_owner }.from(nil)
.to('DOE John')
end
end
end
Test result
I try to rtfm on google but I didn't find the solution yet (I'm always bad for rtfm btw)
Ok my CTO finaly gave me the solution by using the Webmock gem
The code snippet :
before do
stub_request(:get, 'https://api.hubapi.com/crm/v3/owners/876?archived=false&idProperty=id')
.with(headers: { 'Authorization' => "Bearer #{ENV['HUBSPOT_ACCESS_TOKEN']}" }).to_return(status: 200, body: {
firstName: 'John',
lastName: 'Doe'
}.to_json, headers: {})
end
describe '#perform' do
it 'updates contract' do
expect { perform }.to change { contract.reload.hubspot_tailor_deal_owner }.from(nil)
.to('DOE John')
end
end

How to make request specs for rails controller - request not hitting controller method

I am trying to write specs for a controller but my tests don't seem to be hitting the controller action.
My controller:
class CoursesController < ApplicationController
def elearning_course_removal
Rails.logger.warn("warning!")
study_group = StudyGroup.where(id: params[:study_group_id]).first
course = Course.where(id: params[:id]).first
if study_group && course
Workflow.new.remove_elearning_course(study_group, course, current_user.id)
flash[:notice] = t('study_groups.courses.remove_elearning_course.enqueue', course_name: course.name)
else
Rails.logger.error(elearning_removal_error(study_group, course))
flash[:notice] = t('study_groups.courses.remove_elearning_course.enqueue_failure')
end
redirect_to :back
end
end
My test:
require 'spec_helper'
describe CoursesController do
describe '#elearning_course_removal' do
let(:course) { FactoryBot.create(:course_with_files) }
let(:study_group) { FactoryBot.create(:study_group) }
let(:user) { FactoryBot.create(:user) }
let(:params) { {study_group_id: study_group.id, id: course.id, current_user: user} }
it 'logs' do
expect(Rails.logger).to receive(:warn)
# put :elearning_course_removal, params
controller.elearning_course_removal
end
end
Calling the route like this put :elearning_course_removal, params in the spec just fails saying that logger wasn't called. Using the second style, controller.elearning_course_removal will hit my controller method, but it's not standard, and I don't know how I would pass params in this case. Its not a routing issue, as it is identifying the controller variable correctly.
What do I need to be able to write request specs for this controller?
I think that you are not sending properly the params
RSpec.describe CoursesController do
describe "GET elearning_course_removal" do
let(:course) { FactoryBot.create(:course_with_files) }
let(:study_group) { FactoryBot.create(:study_group) }
let(:user) { FactoryBot.create(:user) }
let(:params) { {study_group_id: study_group.id, id: course.id, current_user: user} }
it "logs" do
expect(Rails.logger).to receive(:warn)
put :elearning_course_removal, params: params
end
end
you can pass params like this
params: {study_group_id: study_group.id, id: course.id, current_user: user}
params: params
or directly
put :elearning_course_removal, params: {study_group_id: study_group.id, id: course.id, current_user: user}
what you are doing is this
put :elearning_course_removal, {study_group_id: study_group.id, id: course.id, current_user: user}
params tag is missing which could be the issue

RSpec returns empty string in a fairly simple get request

I want to test show action in my Shipment controller. To do so I've prepared fairly simple specs:
RSpec.describe ShipmentsController, type: :controller do
describe 'GET #show' do
let(:params) { { id: shipment.id, product_id: product.id } }
let!(:product) { create(:product) }
let!(:shipment) { create(:shipment, product: product) }
context 'when params are valid' do
before { get :show, params: params }
it 'return valid json' do
expect(JSON.parse(response.body)).to eq(expected_json)
end
end
end
end
ShimpentsController.rb
class ShipmentsController < ApplicationController
before_action :set_product
attr_reader :shipment
def show
#shipment = Shipment.find(params[:id])
#items = shipment&.grouped_shipment_items
end
private
def set_product
#product = Product.find(params[:product_id])
end
end
When I use postman everything went well - it returns expected json but in the RSpec test I'm getting:
response.body
=> ""
I think you need to add render_views in your controller spec file.
RSpec.describe ShipmentsController, type: :controller do
render_views
describe 'GET #show' do
let(:params) { { id: shipment.id, product_id: product.id } }
let!(:product) { create(:product) }
let!(:shipment) { create(:shipment, product: product) }
context 'when params are valid' do
before { get :show, params: params }
it 'return valid json' do
expect(JSON.parse(response.body)).to eq(expected_json)
end
end
end
end
Reference: https://rubyinrails.com/2019/04/11/rails-test-jbuilder-json-response-with-rspec/
I think you are not making a request for JSON response with rspec. You can check by putting a breakpoint in your controller action, then checking
request.format.json?
In order to ask for JSON response from an rspec test, you should add as: :json to the end of the request. The request should look like this:
get :show, params: params, as: :json

Stub or mock instance method rspec

The below function is a controller action and gets the data to be served from an AWS S3 bucket.
def show
s3_response = Zlib::GzipReader.new(ApiBucket.bucket.object(id).get.body).read
render json: s3_response
end
I am writing a spec for this method. I need to stub/mock such that s3_response doesn't actually request from s3. Here is what I tried. This doesn't work though. Please help.
describe '#GET show' do
let!(:resource) { create(:resource) }
before do
json_data = Api::V2::Presenter.consume_as_json(resource)
allow_any_instance_of(Zlib::GzipReader).to receive(:read).and_return(json_data)
end
it 'should return the resource in page format' do
get :show, format: :json, params: { id: resource.uuid }
response_body_json = JSON.parse(response.body)
json_data = Api::V2::Presenter.consume_as_json(
Api::V2::ResourcePresenter.new(resource).page,
true
)
expect(response_body_json).to eql(JSON.parse(json_data))
end
end
I am getting this error Zlib::GzipFile::Error: not in gzip format
Instead of stubbing Zlib::GzipReader or the S3 Bucket. One simple and easy way to handle such cases would be to create a new private function in the controller and then stub the controller function.
In the controller side:
def show
render json: s3_response
end
private:
def s3_response
Zlib::GzipReader.new(ApiBucket.bucket.object(id).get.body).read
end
The spec will be:
describe '#GET show' do
let!(:resource) { create(:resource) }
before do
json_data = Api::V2::Presenter.consume_as_json(resource)
ResourceController.any_instance.stub(:s3_response).and_return(json_data)
end
it 'should return the resource in page format' do
get :show, format: :json, params: { id: resource.uuid }
response_body_json = JSON.parse(response.body)
json_data = Api::V2::Presenter.consume_as_json(
Api::V2::Presenter.new(resource).page,
true
)
expect(response_body_json).to eql(JSON.parse(json_data))
end
end

Devise Signin Testing with RSpec for API

authenticate_with_http_token do |token, options|
auth_key = AuthKey.find_by(authentication_token: token)
if auth_key.present?
if auth_key.token_valid?
auth_key.touch
sign_in(:user, auth_key.user, store: false, bypass: false) unless current_user.present?
else
render json: { message: t('invalid_otp_access'), errors: [t('token_expired')] }, status: 401 and return
end
else
render json: { message: t('invalid_access_message'), errors: [t('invalid_access')] }, status: 401 and return
end
end
i need to write spec for the above code, in my controller i am using current_user.
My controller looks like below.
def index
schedules = params[:type] == "upcoming" ? :upcoming : :past
schedules = current_user.audit_schedules.send(schedules)
if schedules.present?
paginate json: schedules, per_page:10, root: false, each_serializer: Api::V1::MyAuditSerializer
else
render json: { message: t('.no_audits_scheduled'), errors: [] }
end
end
and i am trying to test my index with passing valid token and params
context "with invalid attributes" do
it "It will return list of audits" do
request.headers["Authorization"] = "Token #{auth_key.authentication_token}"
#request.env["devise.mapping"] = Devise.mappings[:user]
get :index, { params: { type: "upcoming" } }
expect(response.body).to eq 200
end
end
the above spec returning body like
<html><body>You are being redirected.</body></html>
And in my spec helper i included devise helpers like
config.include Devise::TestHelpers, type: :controller
If i remove that helper current_user is always nil. if i add that helper it is redirecting like above, please let me know what i missed and how can i test those spec.
I think you want user_signed_in? vs. current_user.present?. This doesn't fix the problem.
You're sure user is not null? and that the user has been confirmed if you're using confirmable?
Digging through the code, I see this:
if options[:bypass]
warden.session_serializer.store(resource, scope)
elsif warden.user(scope) == resource && !options.delete(:force)
# Do nothing. User already signed in and we are not forcing it.
true
else
warden.set_user(resource, options.merge!(scope: scope))
end
source: https://github.com/hassox/warden/blob/906edf86c6c31be917a921097031b89361d022e8/lib/warden/proxy.rb
You can try adding :force which should force the setting of the user.

Resources