Dynamic rendering with Rails 5 and Rspec - ruby-on-rails

I am trying to test that my controller renders the correct status codes via request specs. This application uses a bit of meta-programming with dynamic class names to render views. How can I stub the render call below to return the correct status code desired for my spec?
Rspec Spec Snippet
context 'renders 200' do
let(:provider_slug) { create(:provider, :active).slug }
let(:template) { "providers/v1/#{provider_slug}/new" }
let(:layout) { "providers/v1/#{provider_slug}" }
let(:provider_double) do
instance_double(
ProviderRouter,
valid?: true,
form_model: ProviderFormModel
)
end
before do
allow(ProviderRouter).
to receive(:new).with(version: 'V1', provider_slug: provider_slug).
and_return(provider_double)
allow(described_class).to receive(:render_new_form).and_return(true)
get route
end
it 'true' do
expect(response.status).to be(200)
end
end
Controller Snippet
class V1::ProvidersController < ApplicationViewController
before_action :init_provider, :init_form_types, :validate_provider
def new
#provider_form_model = provider_router.form_model.new
render_new_form
end
private
attr_reader :provider_slug, :provider_path, :provider_router, :provider_model
def render_new_form
render template: "providers/v1/#{provider_slug}/new", layout: "providers/v1/#{provider_slug}"
end
Updated for Answer Below
context 'renders 200' do
let(:provider_slug) { create(:provider, :active).slug }
let(:provider_double) do
instance_double(
ProviderRouter,
valid?: true,
form_model: ProviderFormModel
)
end
before do
allow(ProviderRouter).
to receive(:new).with(version: 'V1', provider_slug: provider_slug).
and_return(provider_double)
allow(controller).to receive(:provider_slug).and_return(provider_slug)
allow(controller).to receive(:render).and_call_original
allow(controller).to receive(:render).
with(template: "providers/v1/#{provider_slug}/new", layout: "providers/v1/#{provider_slug}") do
controller.render plain: '200 [OK]'
end
get "/v1/providers/#{provider_slug}"
end
it 'true' do
expect(response.status).to be(200)
end
end

The formal answer would be that you shouldn't stub it as you would be stubbing behaviour of the object under test.
You should rather provide a provider_slug to be used for the test.
Technically, it would be possible to do this:
allow(controller) # controller is the instance of the ProvidersController used under the hood
.to_receive(:render_new_form)
.and_return("some bogus value")
But this would lead to rails trying to render the default template as no rendering has happened yet. It would thus be helpful to actually call the render method which can be achived by:
# we call the render method in our stub and thus have to be able to call the original
allow(controller)
.to receive(:render)
.and_call_original
allow(controller)
.to_receive(:render) # not render_new_form
.with(template: anything, layout: anything) do
controller.render plain: '200 [OK]'
end

Related

How to use the allow method in RSpec to mock a function inside a controller that is inside a module (Module > Controller > Function)

I am trying to write the allow method in RSpec. My rails controller is
module Users
class ProfilesController < ApplicationController
# Update user profile
def update
payload = { name: params[:user][:name],email: params[:user][:email]}
response = send_request_to_update_in_company(payload)
if response['code'] == 200
if User.first.update(user_params)
render json: { message: "User successfully updated"}, status: :ok
else
head :unprocessable_entity
end
else
render json: { error: 'Error updating user in Company' },status: :unprocessable_entity
end
end
private
def send_request_to_update_in_comapny(payload)
response = Api::V1::CompanyRequestService.new(
payload: payload.merge(company_api_access_details),
url: 'customers/update_name_email',
request_method: Net::HTTP::Post
).call
JSON.parse(response.body)
end
end
end
When I write the bellow code in my test file
allow(Users::ProfilesController).to receive(:send_request_to_update_in_company).and_return({ 'code' => 500 })
I am getting the following error in terminal
Users::ProfilesController does not implement: send_request_to_update_in_comapny
enter code here
With allow_any_instance_of I am able to get the code working. But how can I implement it using allow?
Yes, allow_any_instance_of works because, as the name suggests, it allows any instance of Users::ProfilesController to respond to the instance method send_request_to_update_in_company with your mock return value.
However, your line
allow(Users::ProfilesController).to receive(:send_request_to_update_in_company)
is telling RSpec to mock a class method called send_request_to_update_in_company, which doesn't exist. And so, you're seeing the error message saying so.
You don't say where your test is situated, but generally wherever it is, it's not a good idea to either test or stub out a private method.
I'd be inclined to instead create a mock Api::V1::CompanyRequestService object to return a fake response, which your controller code can then parse as expected and produce the expected JSON. For example
mock_request = instance_double(Api::V1::CompanyRequestService)
allow(mock_request).to receive(:call).and_return('{"code": 500}')
allow(Api::V1::CompanyRequestService).to receive(:new).and_return(mock_request)
Another approach might be to leave your service alone, and instead use tools like VCR or WebMock to provide mocked JSON values at the network layer - your code can think it's calling out to the internet, but really it gets back responses that you define in your tests.
How about this way:
spec/requests/users/profiles_controller_spec.rb
require 'rails_helper'
RSpec.describe "Users::ProfilesControllers", type: :request do
describe "Test call to special function: " do
let(:controller) { Users::ProfilesController.new }
it "Should response to code 500" do
response = controller.send_request_to_update_in_company("test")
expect(response).to eq({"code"=>"500", "test1"=>"abc", "test2"=>"def"})
end
it "Should return to true" do
response = controller.true_flag?
expect(response).to eq(true)
end
end
end
app/controllers/users/profiles_controller.rb
module Users
class ProfilesController < ApplicationController
# Update user profile
def update
payload = { name: params[:user][:name],email: params[:user][:email]}
response = send_request_to_update_in_company(payload)
Rails.logger.debug "Ok71 = response['code'] = #{response['code']}"
# if response['code'] == 200
# if User.first.update(user_params)
# render json: { message: "User successfully updated"}, status: :ok
# else
# head :unprocessable_entity
# end
# else
# render json: { error: 'Error updating user in Company' },status: :unprocessable_entity
# end
end
# Not private, and not mistake to 'send_request_to_update_in_comapny'
def send_request_to_update_in_company(payload)
response = Api::V1::CompanyRequestService.new(
payload: "for_simple_payload_merge_values",
url: 'for_simple_customers/update_name_email',
request_method: "for_simple_request_method"
).call
Rails.logger.debug "Ok66 = Start to log response"
Rails.logger.debug response
JSON.parse(response.body)
end
# Simple function to test
def true_flag?
true
end
end
end
app/services/api/v1/company_request_service.rb
class Api::V1::CompanyRequestService < ActionController::API
def initialize(payload="test1", url="test2", request_method="test3")
#payload = payload
#url = url
#request_method = request_method
end
def call
#object = Example.new
#object.body = {code: "500", test1: "abc", test2: "def"}.to_json
return #object
end
end
class Example
attr_accessor :body
def initialize(body={code: "000", test1: "init_value_abc", test2: "init_value_def"}.to_json)
#body = body
end
end
I use simple code to simulate your project. Modify it to suitable your working! Tell me about your its thinking. Thank you!

How to test that a class is called in a controller method with RSpec

I am testing my controller to ensure that a library class is called and that the functionality works as expected. NB: This might have been asked somewhere else but I need help with my specific problem. I would also love pointers on how best to test for this.
To better explain my problem I will provide context through code.
I have a class in my /Lib folder that does an emission of events(don't mind if you don't understand what that means). The class looks something like this:
class ChangeEmitter < Emitter
def initialize(user, role, ...)
#role = role
#user = user
...
end
def emit(type)
case type
when CREATE
payload = "some payload"
when UPDATE
payload = "some payload"
...
end
send_event(payload, current_user, ...)
end
end
Here is how I am using it in my controller:
class UsersController < ApplicationController
def create
#user = User.new(user_params[:user])
if #user.save
render :json => {:success => true, ...}
else
render :json => {:success => false, ...}
end
ChangeEmitter.new(#user, #user.role, ...).emit(ENUMS::CREATE)
end
end
Sorry if some code doesn't make sense, I am trying to explain the problem without exposing too much code.
Here is what I have tried for my tests:
describe UsersController do
before { set_up_authentication }
describe 'POST #create' do
it "calls the emitter" do
user_params = FactoryGirl.attributes_for(:user)
post :create, user: user_params
expect(response.status).to eq(200)
// Here is the test for the emitter
expect(ChangeEmitter).to receive(:new)
end
end
end
I expect the ChangeEmitter class to receive new since it is called immediately the create action is executed.
Instead, here is the error I get:
(ChangeEmitter (class)).new(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
What am I missing in the above code and why is the class not receiving new. Is there a better way to test the above functionality? Note that this is Rspec. Your help will be much appreciated. Thanks.
You need to put your expect(ChangeEmitter).to receive(:new) code above the post request. When you are expecting a class to receive a method your "expect" statement goes before the call to the controller. It is expecting something to happen in the future. So your test should look something like:
it "calls the emitter" do
user_params = FactoryGirl.attributes_for(:user)
expect(ChangeEmitter).to receive(:new)
post :create, user: user_params
expect(response.status).to eq(200)
end
EDIT
After noticing that you chain the "emit" action after your call to "new" I realized I needed to update my answer for your specific use case. You need to return an object (I usually return a spy or a double) that emit can be called on. For more information on the difference between spies and doubles check out:
https://www.ombulabs.com/blog/rspec/ruby/spy-vs-double-vs-instance-double.html
Basically a spy will accept any method called on it and return itself whereas with a double you have to specify what methods it can accept and what is returned. For your case I think a spy works.
So you want to do this like:
it "calls the emitter" do
user_params = FactoryGirl.attributes_for(:user)
emitter = spy(ChangeEmitter)
expect(ChangeEmitter).to receive(:new).and_return(emitter)
post :create, user: user_params
expect(response.status).to eq(200)
end

Controller test with stub using Rails 4 and Rspec 3.4

In my controller I have the following method:
def set_provider
#provider = Provider.find_by(id: params[:id])
return #provider unless #provider.active
#provider = IntegrationProvider.new(#provider.slug).proxy_to.find_by(id: params[:id])
end
Which I am trying cover with the following test:
context 'active' do
before do
#provider = FactoryGirl.create(:provider, :active)
get :show, id: #provider.id
end
it '200' do
expect(response.status).to be(200)
end
it 'assigns #provider' do
expect(assigns(:provider)).to eq(#provider)
end
end
The IntegrationProvider class is basically a child model of the Provider model. Since these can be dynamic, and I am using FactoryGirl I am thinking a stub would work best here. In the code base the slugs correspond to folders so I do not want to test that other class in this controller class. I will save that for my lib class tests.
Question
How would you stub that IntegrationsProvider class so it returns the Provider model?
Something like this should work:
allow(IntegrationProvider).to receive_message_chain(:new, :proxy_to, :find_by).and_return(yourproviderobjecthere)

Rails 4 - rendering JSON from a view

I'm having the worst time rendering a .json.erb file from my controller while being able to test it with RSpec. I have api_docs/index.json.erb and the following controller:
class ApiDocsController < ApplicationController
respond_to :json
def index
render file: 'api_docs/index.json.erb', content_type: 'application/json'
end
end
The explicit render file line seems unnecessary, but if I don't do that or render template: 'api_docs/index.json.erb', then I get an error about "Missing template api_docs/index". Likewise if I do have to pass the file name, it sucks even more that I have to give the exact directory--Rails should know that my ApiDocsController templates live in the api_docs directory.
If I have render file or render template, then I can visit the page and get the JSON contents of my index.json.erb file, as expected. However, this RSpec test fails:
let(:get_index) { ->{ get :index } }
...
describe 'JSON response' do
subject {
get_index.call
JSON.parse(response.body)
}
it 'includes the API version' do
subject['apiVersion'].should_not be_nil
end
end
It fails on the JSON.parse(response.body) line and if I raise response.body, it's an empty string. If I do render json: {'apiVersion' => '1.0'}.to_json in the controller, then the test passes just fine.
So, how can I always render the JSON template when I go to /api_docs (without having to put .json at the end of the URL), and in a way that works both in the browser and in my RSpec test? And can I render the template without having to have some long render call in which I pass the full path of the view?
Actually since you're already using respond_to :json in your controller you can use just a render method to choose your template and, as you probably know, if the template have the same name of the controller method you should be able to suppress the whole render method.
If you just remove the render line, what's the result?
Part of my solution was based on this answer to another question: adding defaults: {format: :json} to my routes file lets me go to /api_docs and see the JSON when the action is just def index ; end with no render. The RSpec test still fails though. The full line from my routes file: resources :api_docs, only: [:index], defaults: {format: :json}.
Thanks to this guy with the same problem and his gist, I added render_views to my describe block and got my test to pass:
describe ApiDocsController do
render_views
...
let(:get_index) { ->{ get :index } }
describe 'JSON response' do
subject {
get_index.call
JSON.parse(response.body)
}
it 'includes the API version' do
subject['apiVersion'].should_not be_nil
end
end

Testing instance variables in controller with RSpec

Given a controller like this where it creates several instance variables for use by the view, would you generally test that each of those get set properly? It seems like you would want to, but it also seems a little it could be a bit tricky. What's the right approach?
class StaffsController < ApplicationController
def index
set_index_vars
#all_staff = Staff.find_staff_for_business_all_inclusive(current_business_id)
respond_to do |format|
format.html { render :action => "index", :locals => { :all_staff => #all_staff, :all_services => #all_services, :new_vacation => #new_vacation } }
end
end
def set_index_vars
#days_of_week = days_of_week
#first_day_of_week = DefaultsConfig.first_day_of_week
#all_services = Service.services_for_business(current_business_id)
#new_vacation = StaffVacation.new
#has_hit_staff_limit = current_user_plan.has_hit_staff_limit?
end
end
The code is also posted at https://gist.github.com/1018190
If you're going to write a controller spec, then yes, by all means test that the instance variables are assigned. Much of the 'trickiness' can come from dependencies on other models/libraries, so stub out those method calls:
# air code
Staff.stub(:find_staff_for_business_all_inclusive) {array_of_staff}
controller.stub(:days_of_week) {['Monday','Tuesday',....etc...]}
DefaultsConfig.stub(:first_day_of_week) {"Monday"}
Service.stub(:services_for_business).with(some_value_for_the_current_business_id).\
and_return(some_relevant_value)
StaffVacation.stub(:new) {something_meaningful}
controller.stub_chain(:current_user_plan,:has_hit_staff_limit?) {false}
get :index
assigns(:days_of_week).should == ['Monday','Tuesday',....etc...]
# ...etc...
I would split it up as follows: test that the index calls the correct method. Then test whether the method works.
So something like
describe StaffsController do
describe "GET #index" do
it "calls set_index_vars" do
controller.should_receive(:set_index_vars)
get :index
end
# and your usual tests ...
end
describe "#set_index_vars" do
before(:each) do
# stub out the code not from this controller
controller.stub_chain(:current_user_plan, :has_hit_staff_limit?).and_return(false)
.. etc ..
controller.set_index_vars
end
it { assigns(:days_of_week).should == controller.days_of_week }
it { assigns(:has_hit_staff_limit).should be_false
# etc ..
end
end
Hope this helps.
So long as you have good coverage around your method, you can test that your method is being called at the right times, with the right values etc. Something like:
describe StaffsController do
describe "GET #index" do
it "should call set_index_vars" do
controller.should_receive(:set_index_vars)
get :index
end
end
describe "#set_index_vars" do
it "should assign instance variables with correct values" do
# or wtv this is supposed to do
get :index
assigns(:days_of_week).should == controller.days_of_week
# etc ..
end
end
end

Resources