Rails testing controller private method with params - ruby-on-rails

I have a private method in a controller
private
def body_builder
review_queue = ReviewQueueApplication.where(id: params[:review_queue_id]).first
...
...
end
I would like to test just the body_builder method, it is a method buidling the payload for an rest client api call. It needs access to the params however.
describe ReviewQueueApplicationsController, type: :controller do
describe "when calling the post_review action" do
it "should have the correct payload setup" do
#review_queue_application = ReviewQueueApplication.create!(application_id: 1)
params = ActionController::Parameters.new({ review_queue_id: #review_queue_application.id })
expect(controller.send(:body_builder)).to eq(nil)
end
end
end
If I run the above it will send the body_builder method but then it will break because the params have not been set up correctly as they would be in a call to the action.
I could always create a conditional parameter for the body_builder method so that it either takes an argument or it will use the params like this def body_builder(review_queue_id = params[:review_queue_id]) and then in the test controller.send(:body_builder, params), but I feel that changing the code to make the test pass is wrong it should just test it as it is.
How can I get params into the controller before I send the private method to it?

I think you should be able to replace
params = ActionController::Parameters.new({ review_queue_id: #review_queue_application.id })
with
controller.params = ActionController::Parameters.new({ review_queue_id: #review_queue_application.id })
and you should be good. The params is just an attribute of the controller (the actual attribute is #_params but there are methods for accessing that ivar. Try putting controller.inspect in a view).

Related

Best way to sanitize params in controller spec

I use RSpec with FactoryGirl (to build my model) to test a controller in my application and the test fails there :
subject do
post :create, params: {
my_model: attributes_for(:my_model,
:pictures_for_post_request)
}, session: { user_id: user.id }
end
it 'return a 200 status response' do
subject
expect(response).to have_http_status 200
end
When the test fails it returns an http status code 400 because in my model's validation I check if an attributes of this model is between two integer values and the value passed as param is a string.
But in my controller I parse my params to get proper integers :
private
def sanitize_params
[my_params_keys].each do |k|
params[k] = params[k].to_i if params[k].nil?
end
end
My question is : How to properly sanitize/.to_i my params in this controller spec without rewrite my function in this spec ?
I think it would be best if the model knows how to handle the data rather than relying on the data being formatted the way you expect it. This is the point of model validation. As you can see, based on your test, the params can be anything even though you attempt to sanitize it.
class MyModel
before_validation :convert_my_attribute_to_integer
private
def convert_my_attribute_to_integer
self.my_attribute = self.my_attribute.to_i
end
end
This way your model can be used in multiple contexts without you worrying if the input is properly formatted.

How to test a rails PORO called from controller

I have extracted part of my Foos controller into a new rails model to perform the action:
foos_controller.rb
class FoosController < ApplicationController
respond_to :js
def create
#foo = current_user.do_something(#bar)
actioned_bar = ActionedBar.new(#bar)
actioned_bar.create
respond_with #bar
end
actioned_bar.rb
class ActionedBar
def initialize(bar)
#bar = bar
end
def create
if #bar.check?
# do something
end
end
end
I got it working first but now I'm trying to back-fill the rspec controller tests.
I'll be testing the various model methods and will be doing a feature test to make sure it's ok from that point of view but I would like to add a test to make sure the new actioned_bar model is called from the foos controller with #bar.
I know in rspec you can test that something receives something with some arguments but I'm struggling to get this to work.
it "calls ActionedBar.new(bar)" do
bar = create(:bar)
expect(ActionedBar).to receive(:new)
xhr :post, :create, bar_id: bar.id
end
This doesn't work though, the console reports:
NoMethodError:
undefined method `create' for nil:NilClass
which is strange because it only does this when I use expect(ActionedBar).to receive(:new), the rest of the controller tests work fine.
If I try to do:
it "calls ActionedBar.new(bar)" do
bar = create(:bar)
actioned_bar = ActionedBar.new(bar)
expect(actioned_bar).to receive(:create).with(no_args)
xhr :post, :create, bar_id: bar.id
end
the console says:
(#<ActionedBar:0xc8f9f74>).create(no args)
expected: 1 time with no arguments
received: 0 times with no arguments
If I do a put in the controller whilst running the test; for some reason this test causes the actioned_bar in the controller to be output as nil but fine for all the other controller tests.
Is there any way I can test that ActionedBar is being called in this controller spec?
You can use expect_any_instance_of(ActionedBar).to receive(:create), because instance in spec and in controller are different instances.
If you want to use original object, you can use expect(ActionedBar).to receive(:new).and_call_original (without that #new just will return nil and you'll get NoMethodError).
You can set up a double ActionedBar which is returned by the ActionedBar.new call as this instance is different to the one used in the controller.
describe "#create" do
let(:actioned_bar) { double(ActionedBar) }
let(:bar) { double(Bar) }
it "calls ActionedBar.new(bar)" do
expect(ActionedBar).to receive(:new).with(bar).and_returns(actioned_bar)
expect(actioned_bar).to receive(:create)
xhr :post, :create, bar_id: bar.id
end
end
The core problem is that actioned_bar in your spec in is not going to be the the same instance of ActionedBar that is in your controller. Thus the spec will always fail.
Instead you need to have ActionedBar return a double when new is called:
it "calls ActionedBar.new(bar)" do
bar = create(:bar)
actioned_bar = instance_double("ActionedBar")
allow(ActionedBar).to receive(:new).and_return(actioned_bar)
expect(actioned_bar).to receive(:create).with(no_args)
xhr :post, :create, bar_id: bar.id
end
However I generally consider this kind of test a code smell - its ok to mock out external collaborators and set expectations that you are passing the correct messages. But your might want to consider if your are testing the details of how your controller does its job and not the actual behavior.
I find it better to setup a spec which calls the controller action and set expectations on what how it for example changes the database state or how it effects the response.

Rspec test of the object method call in controller

I want to test that a controller create and update actions call a process method on the MentionService instance.
In controller I have MentionService.new(post).process
This is my current spec:
it "calls MentionService if a post has been submitted" do
post = Post.new(new_post_params)
mention_service = MentionService.new(post)
expect(mention_service).to receive(:process)
xhr(:post, :create, company_id: company.id, remark: new_post_params)
end
In the controller actions I have:
def create
...
# after save
MentionService.new(#remark).process
...
end
What I get is:
expected: 1 time with any arguments
received: 0 times with any arguments
Any ideas?
Thanks.
The problem is that you're creating a new instance in your test and expect that instance to receive :process which will not work.
Try playing around with this snippet:
let(:service) { double(:service) }
it "calls MentionService if a post has been submitted" do
expect(MentionService).to receive(:new).with(post).and_return(service)
expect(service).to receive(:process)
xhr(:post, :create, company_id: company.id, remark: new_post_params)
end
You need to tell your MentionService class to receive :new and return a mock object, which will receive :process. If that is the case, you know the call sequence succeeded.
If you're not interested with supplying the mock object yourself you can also modify your expectation to:
expect_any_instance_of(MentionService).to receive(:process)

Expect to receive working for any_instance but not for specific one

I have the following rspec:
context 'when there is an incoming error' do
it 'should update status of url to error' do
url = create(:url)
error_params = {error: 'whatever', url_id : url.id}
expect(url).to receive(:set_error)
post :results, error_params
end
end
And the results action looks like this:
def results
url = Url.find(url_id: params['url_id'])
if params.key?('error') && !params['error'].blank?
url.set_error
end
end
If I do it like this, the test does not pass:
expected: 1 time with any arguments
received: 0 times with any arguments
However, if I change to:
expect_any_instance_of(Url).to receive(:set_error).
It passes. I just have one Url, so I am not sure what is going on.
When you create a to receive expectation, it is connected to a specific Ruby object.
When the results action is called, it instantiates a new url object. It represents the same database object that you called your expectation on in the Rspec example. But it isn't the same Ruby object - it's a new object with (probably) the same data. So the expectation fails.
To illustrate:
describe ".to_receive" do
it "works on Ruby objects" do
url = Url.create(:url)
same_url = Url.find(url.id)
expect(url).to_not receive(:to_s)
same_url.to_s
end
end
To (somewhat) get the desired behaviour you could use any_instance and change the controller so that it assigns the url object to an instance variable. In that way you can inspect the url object easier:
# Change your action so that it saves the url object as an instance variable
def results
#url = Url.find(url_id: params['url_id'])
if params[:error].present?
#url.set_error
end
end
# Change your spec to look at the assigned variable
context 'when there is an incoming error' do
it 'should update status of url to error' do
url = create(:url)
error_params = {error: 'whatever', url_id: url.id}
expect_any_instance_of(Url).to receive(:set_error)
post :results, error_params
expect(assigns(:url)).to eq(url)
end
end
Since assigns only lets you inspect the assigned ivar after the controller action has been executed, you can't use it to create a receive expectation.

Rspec controller test - how to pass arguments to a method I'm testing

I want to test this method in my controller.
def fetch_match_displayed_count(params)
match_count = 0
params.each do |param|
match_count += 1 if param[1]["result"] && param[1]["result"] != "result"
end
match_count
end
This is the test I've written so far.
describe "fetch_match_displayed_count" do
it "should assign match_count with correct number of matches" do
params = {"result_1"=>{"match_id"=>"975", "result"=>"not_match"}, "result_2"=>{"match_id"=>"976", "result"=>"match"}, "result_3"=>{"match_id"=>"977", "result"=>"not_sure"}, "result_4"=>{"match_id"=>"978", "result"=>"match"}, "result_5"=>{"match_id"=>"979", "result"=>"not_match"}, "workerId"=>"123", "hitId"=>"", "assignmentId"=>"", "controller"=>"mt_results", "action"=>"create"}
controller.should_receive(:fetch_match_displayed_count).with(params)
get :fetch_match_displayed_count, {params:params}
assigns(:match_count).should == 5
end
end
My problem seems to lie in this line get :fetch_match_displayed_count, {params:params}
The method is expecting params, but is getting nil.
I have two questions.
Should this method be in a helper and not in the controller itself (per Rails convention)?
How do I submit a get request and pass params in my test?
As a general rule, you should test the public interface of your class. For a controller, this means you test actions, not helper methods.
You should be able to make this work by setting up one or more separate test cases that call the appropriate action(s), then use a message expectation to test that the helper method is called with the right arguments -- or test that the helper method does what it is supposed to do (sets the right instance variables/redirects/etc).

Resources