11

Looks like a bug in RSpec but maybe I'm missing something.

I have a request spec where I post a JSON that contains an array of hashes:

spec/requests/dummy_request_spec.rb:

post "http://my.server.com/some/route", { format: :json, data: [ { details: { param1: 1 }, }, { details: { param2: 1 } } ] } 

For some odd reason, RSpec merges the hashes into one element and then sends them to server. print out of params received in controller:

data: [ { details: { param1: 1, param2: 2 }, }, ] 

versions: rspec-2.13.0 rails-3.2.10

Very strange!!

Thanks

3 Answers 3

21

Got it! array of hashes is not supported for form-data
RSpec by default posts it as form-data. Solution:

post '...', {...}.to_json, {'CONTENT_TYPE' => "application/json", 'ACCEPT' => 'application/json'} 
Sign up to request clarification or add additional context in comments.

1 Comment

Side note: array of hashes for form-data is supported, but requires 'index' field, otherwise entities will be merged into one. This may be useful if you want to send array of files.
1

I faced the same problem reported in the question post while using following versions

ruby 2.3.2

rails (5.0.0.1)

rspec-rails (3.5.2)

Searching for the problem on web I found a related issue at https://github.com/rails/rails/issues/26069 and the solution suggested by it is to pass as: :json option to the post, get etc methods while using them in the controller tests (refer the PR referenced in comment https://github.com/rails/rails/issues/26069#issuecomment-240916233 for more details). Using that solution didn't solved the params mingling issue I was encountering. Following were the results found for different types of data I used with the recommended solution:

In my controller spec I have following

before(:each) do request.accept = "application/json" end 

and in the test logs I do see that the request is being served

Processing by Api::V1::MyController#my_action as JSON 

Data-1

data = [ { param_1: "param_1_value", }, { param_2: "param_2_value", } ] params.merge!(my_data: data) post :my_action, params: params, as: :json 

That ends-up in request params as following

{ "my_data"=> [ {"param_1"=>"param_1_value", "param_2"=>"param_2_value"} ] } 

which is wrong.

Data-2

data = [ { param_1: "param_1_value", something_else: "" }, { param_2: "param_2_value", another_thing: "" } ] params.merge!(my_data: data) post :my_action, params: params, as: :json 

That ends-up in request params as following

{ "my_data"=> [ {"param_1"=>"param_1_value", "something_else"=>"", "another_thing"=>"", "param_2"=>"param_2_value"} ] } 

which is wrong.

Data-3

data = [ { param_1: "param_1_value", param_2: "" }, { param_1: "" param_2: "param_2_value", } ] params.merge!(my_data: data) post :my_action, params: params, as: :json 

That ends-up in request params as following

{ "my_data"=>[ {"param_1"=>"param_1_value", "param_2"=>""}, {"param_1"=>"", "param_2"=>"param_2_value"} ] } 

which is correct.

It should be noted that for Data-3 without the as: :json option also I receive the correct data in request params.

One more thing: In comment https://github.com/rails/rails/issues/26069#issuecomment-240358290 an alternate solution suggested to deal with the problem narrated above is following

another fix would be to specify nested attributes not as array but as hash:

params = { id: @book.id, book: { title: 'Cool', pages_params: { "0" => { id: @page.id, content: 'another content' }, "1" => { id: @delete_page.id, _destroy: 1 }, "2" => { content: 'another new page' } } }, format: :json } 

Unfortunately this was removed from the documentation of nested attributes so I don't know if this is going to stay valid. http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

But this solution has a drawback is that we need to manually sanitize the data on controller-end to bring it back to expected structure i.e. Array of Hashes.

Finally I am sharing below what worked for me

spec/shared_contexts.rb

RSpec.shared_context "common helpers" do def set_request_header(request_obj:, request_header_name:, request_header_value:) request_obj.headers[request_header_name] = request_header_value end def set_request_header_content_type_as_json(request_obj:) set_request_header(request_obj: request_obj, request_header_name: 'CONTENT_TYPE', request_header_value: 'application/json') end end 

Then in my spec file

require 'shared_contexts' RSpec.describe Api::V1::MyController, :type => :controller do include_context "common helpers" context "POST #my_action" do it "my example" do data = [ { param_1: "param_1_value", }, { param_2: "param_2_value", } ] params.merge!(my_data: data) set_request_header_content_type_as_json(request_obj: request) post :my_action, params: params end end end 

As can be seen setting the request header CONTENT_TYPE was what was missing to make the request params to be received in expected structure.

Comments

0

Also, be aware that you have an extra comma:

data: [ { details: { param1: 1 }**,** }, { details: { param2: 1 } } ] 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.