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.
Related
I am trying to write two RSpec tests for two different problems that are much more advanced that what I'm used to writing.
What I'm trying to test within my controller:
def index
#buildings ||= building_class.active.where(place: current_place)
end
My attempt at writing the RSpec test:
describe 'GET :index' do
it "assigns #buildings" do
#buildings ||= building_class.active.where(place: current_place)
get :index
expect(assigns(:buildings)).to eq([building])
end
end
This test failed and wouldn't even run so I know I'm missing something.
My second test is needing to test the returned value of a class method. Here is what I am needing to test within the controller:
def class_name
ABC::Accountant::Business
end
Here is my attempt at testing this method:
describe "class name returns ABC::Accountant::Business" do
subject do
expect(subject.class_name).to eq(ABC::Accountant::Business)
end
end
For the first test I would do something like this:
First, I would move that .active.where(place: current_place) to a scope (I'm guessing building_class returns Building or something like that):
class Building << ApplicationRecord
scope :active_in, -> (place) { active.where(place: place)
Then it's easier to stub for the test
describe 'GET :index' do
it "assigns #buildings" do
scoped_buildings = double(:buildings)
expect(Building).to receive(:active_in).and_return(scoped_buildings)
get :index
expect(assigns(:buildings)).to eq(scoped_buildings)
end
end
Then your controller will do
#buildings ||= building_class.active_in(current_place)
This way you are testing two things: that the controller actually calls the scope and that the controller assigns the returned value on the #buildings variable (you don't really need to test the actual buidlings, you can test the scope on the model spec).
Personally, I feel like it would be better to do something like #buildings = current_place.active_buildings using the same idea of the scope to test that you are getting the active buildings of the current place.
EDIT: if you can't modify your controller, then the stubbing is a little different and it implies some chaining of methods that I don't like to explicitly test.
scoped_buildings = double(:buildings)
controller.stub_chain(:building_class, :active, :where).and_return(scoped_building)
get :index
expect(assings(:buildings)).to eq scoped_buildings
Note that now your test depends on a specific implementation and testing implementation is a bad practice, one should test behaviour and not implementation.
For the second, I guess something like this should work:
describe ".class_name" do
it "returns ABC::Accountant::Business" do
expect(controller.class_name).to eq(ABC::Accountant::Business)
end
end
IMHO, that the method's name if confusing, class_name gives the idea that it returns a string, you are not returnin a name, you are returning a class. Maybe you can change that method to resource_class or something less confusing.
Introduction
I have a hierarchy parent-children in my RoR project
and I wrote a new feature to our ruby on rails project, that shows two random children records from a parent.
class Article < ActiveRecord::Base
has_many :reviews
def to_json
{
...
reviews: reviews.n_random.as_json
...
}
end
end
and
class Review < ActiveRecord::Base
belongs_to :article
scope :n_random, ->(n=2) { order("RANDOM()").limit(n) }
end
Now, the problem that I have is even though the randomness works correctly, even in tests, I have problems with few tests that actually test this feature indirectly.
Let's say that I have an ArticlesControllerTest test suite, that contains a method
test 'show renders correct article' do
# given
params = { format: :json, id: 1 }
article = Article.find(params[:id])
# when
post :get, params
response_article = JSON.parse(response.body, symbolize_names: true)
#then
assert_response 200
assert_equal response_article, article.to_json
end
Problem
The last assert_equal fails, because for example:
response_article contains ids 1, 2
article.to_json contains ids 1, 3
Question
Is it possible to write some kind of a filter, that makes postgres's RANDOM() return always constant value? I know that I can use SELECT setseed(0.5); to set seed, so that next SELECT RANDOM(); returns the same value (although the next RANDOM() will change), but what I would like to achieve is to do something like setseed(0.5) before every possible select from active records.
I'll gladly take any other responses that will help me with this problem, because I know that RoR and Postgres are two different servers and I have no idea how to test this randomness from postgres's side.
inb4: I don't want to modify tests in a huge way.
You should probably use mocks / stubs for this, ensuring a consistent value just for the scope of this test. For example, with Mocha:
Article.any_instance.stubs(:to_json).returns({
...
reviews: reviews.last(2).as_json,
...
})
Or
Review.expects(:n_random).returns(Review.last(2))
And, in this example, you can revoke these using, for example:
Article.any_instance.unstub(:to_json)
N.B. I'm not certain of the syntax for the :n_random stub on a class as I've not got the environment to test it, but hopefully you get the idea (source here).
This means, within your test you will see consistent data, overriding the RANDOM() ordering. That way you can test your controller is doing what's expected of it, without worrying about the random data being used outside of the test env.
To implement, simply include one of the above in your test, i.e.
test 'show renders correct article' do
Review.expects(:n_random).returns(Review.last(2))
# given
params = { format: :json, id: 1 }
article = Article.find(params[:id])
# when
post :get, params
response_article = JSON.parse(response.body, symbolize_names: true)
#then
assert_response 200
assert_equal response_article, article.to_json
end
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).
I have a an json Api who received parameters to create a Device, like name, imei, etc. The Device can have one Blacklist object (has_one :blacklist). I would like to know what's the proper-way to create the blacklist object if a params is present in the post request of Device.
Exemple curl -X POST -d api_key=000000 -d device[name]='stack' -d device[blacklist]='true' https://www.example.com/api/devices.json
In the code for the moment I should have
def create
#device = Device.new
#device.update_attributes(strong_parameters)
if params[:device]['blacklist'] && params[:device]['blacklist'] == true
#blacklist = Blacklist.new(device_id: #device.id)
end
render :device, status: 201 # will render with jbuilder #device and #blacklist
end
But I don't like it that much :
Too much logic in one controller
Verifying parameters inside is a good practice?
If no parameters are given, how to handle the request? I know that strong parameters should return a 400, but what about #device I just created.
This controller smells for me.
Feedbacks welcome.
The result when doing a PATCH
class DevicesController
before_action :found_device, only: :blacklist # get `#device`
before_action :blacklist_device, only: :blacklist
def blacklist
render :device, status: 200
end
private
def blacklist_device
if (params[:device]['blacklisted'] and
params[:device]['blacklisted'] == true and
#blacklist = BlacklistedDevice.create(device_id: #device.id, organisation_id: current_store.organisation.id))
#device.reload
else
render json: { error: "Missing or incorrect 'blacklisted' parameter" }, status: :unprocessable_entity
end
end
end
Too much logic in the conrtoller ? No
I have also heard a lot 'too much logic in the controller is bad' but this is bullshit or rather I believe the words are not accurate enough.
What that phrase means for me, is that for example, model validations should not be in the controller, and the controller should remain light for very basic REST actions. Controller should only be a bridge between the HTML request and the model. Think of it this way : you may have several controllers modifying the same model. What you would write in EVERY controller, should most likely instead be written in the model as a validation.
But here you're dealing with specific requests (transforming a device[blacklist] == true as a Blacklist Model isn't something "natural", so yes in my opinion it should be in the controller.
Plus, a controller action of just 6 lines isn't what we could call "too much logic"
Verifying parameters inside is good Practice ? Yes/No
I assume by that you mean writing specific lines of codes in the controller like if params[xxx] == blabla or something equivalent
The way you did was good. You use specific code only for the special parameter (the blacklist) and the rest of the params go into the model as strong params, so the model validations will do the rest.
Verify parameters only if it's relevant to this particular controller (for example, if it was site-based, you could probably use a different implementation of the blacklist so the difference would have to be in the controller.
If no parameters are given, how to handle the request? I know that strong parameters should return a 400, but what about #device I just created.
This the part I don't quite like about your current implementation. You don't check for the success of your save operations. Here's what you could have written (check the result of every persistence operation result, and render appropriately)
def create
#device = Device.new
if #device.update_attributes(strong_parameters)
if (params[:device]['blacklist']
and params[:device]['blacklist'] == true
and #blacklist = Blacklist.create(device_id: #device.id))
# Handle stuff when everything is cool
render :device, status: 201 # will render with jbuilder #device and
else
# Handle stuff when there's no blacklist param true
end
else
# Handle error on model save
end
end
Inspecting params is well put in the controller - that's it's purpose - the model layer should not have knowledge of request parameters.
But you can put this info in a transient attribute with
class Device
attr_accessor 'create_blacklisted'
end
Then you can create an input field for that new attribute and an after_initialize callback in the Device model as well that can subsequently create the Blacklist entry.
I have a Controller:
class ThingController < ActionController
respond_to :json
def create
puts "CREATE " + params.inspect
end
end
and a test:
require "spec_helper"
describe "/thing" do
context "create" do
it "should get params" do
params = {"a" => "b", "c" => ["d"], "e" => [], "f"=>"",
"g"=>nil, , "controller" => "NOPE", "action" => "NOPE"}
post uri, params
end
end
end
When I run this, the following is logged:
CREATE {"a"=>"b", "c"=>["d"], "action"=>"create", "controller"=>"thing"}
My questions are:
where did e go? I would expect it to deserialize to an empty array, not to nothing at all.
why are the action and controller params being mixed into this? Aren't the body and the rails internals completely separate concerns?
Because of this, my action and controller JSON fields were over-written. How would I access these?
Is this therefore not the right way to accept JSON?
I'm new to Rails, but I have done a lot of Django.
There are two parts to this problem: you need to ensure that your parameters are being sent as JSON, and also that they are being interpreted as JSON.
Essentially, you have to
encode your parameters as JSON
set appropriate content-type and accepts headers
See POSTing raw JSON data with Rails 3.2.11 and RSpec for the way.
The rails middleware will add the action and controller params so you'll have to put those in a nested hash if you still want to access your custom values.
Try adding format: 'json' to the params in your test. This will send a different content-type header and might help serialize the params correctly in order to keep the e param.