I plan to use JSON data in both request and response in my project and having some problems in testing.
After searching for a while, I find the following code which uses curl to post JSON data:
curl -H "Content-Type:application/json" -H "Accept:application/json" \
-d '{ "foo" : "bar" }' localhost:3000/api/new
In the controller I can access the JSON data simply using params[:foo] which is really easy. But for functional testing, I only find post and xhr (alias for xml_http_request).
How can I write functional test in rails to achieve the same effect as using curl? Or should I do test in other ways?
Here's what I've tried. I find the implementation for xhr in action_controller/test_case.rb, and tried to add jhr method simply changing 'Conetent-Type' and 'HTTP_ACCEPT'. (Added in test/test_helpers.rb.)
def json_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
#request.env['Content-Type'] = 'Application/json'
#request.env['HTTP_ACCEPT'] ||= [Mime::JSON, Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
__send__(request_method, action, parameters, session, flash).tap do
#request.env.delete 'Content-Type'
#request.env.delete 'HTTP_ACCEPT'
end
end
alias jhr :json_http_request
I used this in the same way as xhr, but it does not work. I inspected the #response object and sees the body is " ".
I also find one similar question on Stack Overflow but it's for rails 2 and the answer for posting raw data does not work in rails 3.
As of Rails 5, the way to do this is:
post new_widget_url, as: :json, params: { foo: "bar" }
This will also set the Content-type header correctly (to application/json).
I found that this does exactly what I want – post JSON to a controller's action.
post :create, {:format => 'json', :user => { :email => "test#test.com", :password => "foobar"}}
Just specify appropriate content type:
post :index, '{"foo":"bar", "bool":true}', "CONTENT_TYPE" => 'application/json'
Json data should go as a string, not as a Hash.
Looking at stack trace running a test you can acquire more control on request preparation:
ActionDispatch::Integration::RequestHelpers.post => ActionDispatch::Integration::Session.process =>
Rack::Test::Session.env_for
Specifying :format does not work because request go as 'application/x-www-form-urlencoded' and json isn't parsed properly processing a request body.
Assuming you have a controller named api, a method named new, and you're in the test for the api controller:
#request.env["RAW_POST_DATA"] = '{ "foo" : "bar" }'
post :new
did the trick for me.
Here is a snippet that let me post json data to test my own app. rails 3
port = Rails.env.production? ? 80 : 3000
uri = URI.parse( Rails.application.routes.url_helpers.books_url(:host => request.host, :port => port, :format => :json) )
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri)
request.content_type = 'application/json'
request.body = #json_data
response = http.request( request )
#result = response.body
Hope this helps others
As #taro suggests in a comment above, the syntax that works for me in functional and integration tests is:
post :create, {param1: 'value1', param2: 'value2', format: 'json'}
(The curly braces aren't always necessary, but sometimes it doesn't work if they're missing, so I always add them.)
Here's what params and request.format look like for a post of that sort:
params:
{"param1"=>"value1", "param2"=>"value2", "format"=>"json", "controller"=>"things", "action"=>"create"}
request.format:
application/json
The best answer I can come up with to this is you don't
Whether or not it was intentional it s maybe good that rails doesn't implement this for you.
In functional tests you really want to just test your controller and not rails method of deserialization or even that routing and mime detection are all setup correctly, those all fall under an IntegrationTest.
So for your controllers, don't pass JSON just pass your params hash like you normally would. Maybe adding :format as an argument as well if you need to check that and respond differently.
If you want to test the full stack move to an IntegrationTest
Related
I want to test uploading a file in a Rails Rspec request test. I'm using Paperclip for asset storage.
I've tried:
path = 'path/to/fixture_file'
params = { file: Rack::Test::UploadedFile.new(path, 'application/pdf', true) }
post v1_product_documents_path, params: params
In the controller, I get a string
"#Rack::Test::UploadedFile:0x0055b544479128>"
instead of the actual file.
The same code works in controller tests
try to use fixture_file_upload:
fixture_file_upload
or
if you wanted to use this in a factory
Rack::Test::UploadedFile.new(File.open(File.join(Rails.root, '/spec/fixtures/images/bob-weir.jpg')))
In rails_helper.rb do
include ActionDispatch::TestProcess
include Rack::Test::Methods
Then you have few options.
1. You can use fixture_file_upload helper
let(:file) { fixture_file_upload('files/image.jpg') }
it 'it should work' do
post some_path, params: { uploads: { file: file } }
end
You can use Uploaded file but you have to give full path
let(:file) { Rack::Test::UploadedFile.new(Rails.root.join('spec',
'fixtures', 'blank.jpg'), 'image/jpg') }
let(:params) { { attachment: file, variant_ids: ['', product.master.id] } }
let(:action) { post :create, product_id: product.slug, image: params }
For rails 6 no need to do includes, just fixture_file_upload. If you are using spec/fixtures/files folder for fixtures you can use file_fixture helper
let(:csv_file) { fixture_file_upload(file_fixture('file_example.csv')) }
subject(:http_request) { post upload_file_path, params: { file: csv_file } }
Under describe block, include these modules
include Rack::Test::Methods
include ActionDispatch::TestProcess
Now try running the specs
path = 'path/to/fixture_file'
params = { "file" => Rack::Test::UploadedFile.new(path, 'application/pdf', true) }
post v1_product_documents_path, params: params
Also, I guess you forgot to add , between v1_product_documents_path and params: params, please add that and let me know.
Hope that helps!
Might be useful for other users: I got this problem because I mistakenly used a get instead of a post request in my specs.
For others still getting something like "#Rack::Test::UploadedFile:0x0055b544479128>" in the controller after implementing the approved solution above, I resolved this by changing my content-type header to be 'application/x-www-form-urlencoded' so that form data would be accepted.
file = File.expand_path("path/to/fixture_file", __dir__)
Rack::Test::UploadedFile.new(file, 'application/pdf')
If you need the content type in your RSpec test, it seems you can pass it as an argument to fixture_file_upload (no idea why this method does not derive the content type from the OS).
For instance:
fixture_file_upload("pdfs/some.pdf", "application/pdf")
I ran into this issue while writing request specs for a POST endpoint that processes incoming email payloads that include file attachments.
this is what the request looked like in the request spec:
post api_v1_email_processor_path, params: params, as: :json
where params[:attachment] was:
fixture_file_upload("spec/support/fixtures/files/report.csv", 'text/csv',)
and when I added some breakpoints in the controller, this is what the attachment was being converted into:
{"original_filename"=>"report.csv", "tempfile"=>"#<File:0x0000000121d2bfc8>", "content_type"=>"text/csv"}
as you can see, the "tempfile" was being cast to a string value of "<File:0x0000000121d2bfc8>"
when I emulated the same request in a controller spec, to my huge confusion, there was no casting, and the breakpoint showed the param (correctly) as:
#<ActionDispatch::Http::UploadedFile:0x000000012d559488 #tempfile=#<Tempfile:/var/folders/jg/n_0rknb97kvddb0jgh6r3_x40000gn/T/RackMultipart20221008-45122-mzyqg5.csv>, #original_filename="report.csv", #content_type="text/csv", #headers="Content-Disposition: form-data; name=\"attachment1\"; filename=\"report.csv\"\r\nContent-Type: text/csv\r\nContent-Length: 24931\r\n">]
after seeing L.Youl's answer https://stackoverflow.com/a/67987482/14926068, it got me thinking that the issue was with how the request payload was being formatted, prior to being sent to the server through the Rack middleware. and so I tried taking off the as: :json:
post api_v1_email_processor_path, params: params
and bingo, I got the same result as with the controller spec.
after inspecting response.request.content_type in my request spec, I found that when I took the as: :json off, its value changed from "application/json" to
"multipart/form-data; boundary=----------XnJLe9ZIbbGUYtzPQJ16u1"
for completeness, I checked response.request.content_type in the controller spec as well, and found that even with as: :json in the request, the content_type was being set to "multipart/form-data", and that accounted for the discrepancy in behavior between controller and request specs - it seems that as: :json only affects the "content-type" in the request specs.
in retrospect, it was obvious - json cannot be used to send actual files. but it took me a long time to figure it out because the equivalent controller spec was working fine and this was literally the only endpoint in my Rails api-only app that expected non-json payloads
I am testing a JSON request to our API, It will respond with JSON.
It seems like all the integers within the JSON get converted to strings as we post them to the controller consider action.
Controller
def consider
binding.pry # binding no# 2 used to check the params after post from test.
if ParametersValidator.is_valid?(params)
application_handler = ApplicationHandler.new(request_interactor)
render json: application_handler.result
else
render json: ParametersValidator.failed_params(params).to_json
end
end
The ParamaterValidator validates the structure and types of data coming in.
Test
render_views
let(:json) { JSON.parse(response.body) }
..
..
it 'returns the result in the correct format for the AUTOMATIC APPROVE decision' do
automatic_approve_params = relative_json_file(relative_file('automatic_approve_params'))
expected_approve_params = {
"status" => "accepted",
"automated" => true,
"rate" => 6,
"amount" => 30000,
"term" => 10,
"pre_approved_amount" => 2500,
"comments" => ""
}
#request.headers['HTTP_X_AUTH_SIG'] = Rails.application.secrets['authorization']['token']
request.env["HTTP_ACCEPT"] = 'application/json'
binding.pry # binding no# 1 to inspect the params before post
post :consider, automatic_approve_params, format: :json
expect(json).to eq(expected_approve_params)
end
Binding no#1
{
"student_id"=>1,
"age"=>22,
"name"=>"John",
"age_range"=>"22-25",
"criminal_record"=>false,
"declared_bankrupt"=>false,
"declared_insolvent"=>false,
"declared_sequestrated"=>false,
"defaulted_on_loan"=>false,
"post_study_salary"=>100000000,
"first_nationality"=>"PL",
"second_nationality"=>"",
"citizenship"=>"PL",
}
Binding no#2
{
"student_id"=>"1",
"age"=>"22",
"name"=>"John",
"age_range"=>"22-25",
"criminal_record"=>false,
"declared_bankrupt"=>false,
"declared_insolvent"=>false,
"declared_sequestrated"=>false,
"defaulted_on_loan"=>false,
"post_study_salary"=>"100000000",
"first_nationality"=>"PL",
"second_nationality"=>"",
"citizenship"=>"PL",
}
The test log is showing that the request is
Processing by Api::V1::CreditApplicationsController#consider as JSON
Inspecting the request just before the post action you will see the params are fine, then in the controller before I run anything I inspect the params and they are all strings.
Using postman to test the API with the JSON works as expected but it seems that rspec when posting to the consider action will convert all the params to strings. I have read a few dozen posts that claim by adding format: :json to the post action it will remedy this, however I have had no such luck.
I am obviously doing something wrong but I have tried pretty much everything I know.
After replicating the issue you are having I managed to resolve it in a controller spec using the following:
post :consider, automatic_approve_params.merge(format: :json)
In my local tests I removed the
request.env["HTTP_ACCEPT"] = 'application/json' and it still worked as you expect it to. Hope it helps.
In Rails 5, use as: :json instead of format: :json, e.g. post :consider, params: automatic_approve_params, as: :json
We can try this
post 'orders.json', JSON.dump(order: {boolean: true, integer: 123}), "CONTENT_TYPE" => "application/json"
This is a problem that has been bothering me for some time. I am building an API function that should receive data in json and response in json. My controller tests run fine(Since I abstract that the data gets there already decode from JSON and only the answer needs to be interpreted ).
I Also know that the function runs fine since I have used curl to test it with JSON arguments and it works perfectly.
(ex: curl -i --header "Accept: application/json" --header "Content-Type: application/json" -d '{"test":{"email":"andreo#benjamin.dk"}}' )
But obviously I would like to write request(feature) tests to test this automatically and the way I see it they should work exactly like curl, i.e., hit my service like it was an external call. That means that I would like to pass the arguments in JSON and receive an answer. I am pretty lost since all the examples I can see people treat arguments as it was already decoded.
My question is: I am following a wrong premise in wanting to send the arguments and request as a JSON one since i will be testing that rails works, because this is its responsibility? But I would like to see how robust my code his to wrong arguments and would like to try with JSON.
something of this type:
it "should return an error if there is no correct email" do
params = {:subscription => {:email => "andre"}}
post "/magazine_subscriptions", { 'HTTP_ACCEPT' => "application/json", 'Content-Type' => 'application/json', 'RAW_POST_DATA' => params.to_json }
end
Do you know how this is possible? and please let me know if you think I am testing it wrong.
all the best,
Andre
I found my answer on a post here(RSpec request test merges hashes in array in POST JSON params), I think what I was doing wrong concerned the arguments to the request.
so this worked:
it "should return an error if there is no correct email" do
params = {:subscription => {:email => "andre"}}
post "/magazine_subscriptions", params.to_json, {'ACCEPT' => "application/json", 'CONTENT_TYPE' => 'application/json'}
end
describe '#create' do
let(:email) {'andre'}
let(:attrs) {{email: email}}
let(:params) {{format: :json, subscription: attrs}}
it "should return an error if there is no correct email" do
post "/magazine_subscriptions", params
end
end
I'm developping a post to a callback url in Ruby on Rails and use the Httparty library for this, I receive the post perfectly on the url but it seems that rails convert the data that is pushed to the url 2 times to parameters. Here is the code that I use to do the call :
#result = HTTParty.post("http://localhost:3000/mailchimp/callback/",
:body => {
:data => {
:title => 'This is the screen name'}
}.to_json,
:headers => { 'Content-Type' => 'application/json' } )
In the logs of the receiving application I got this :
Parameters: {"mailchimp"=>{"controller"=>"mailchimp", "action"=>"callback", "data"=>{"title"=>"This is the screen name"}}, "data"=>{"title"=>"This is the screen name"}}
You see directly that I have 2 times the data parameters, once in the controller hash and once in the normal parameters hash. How does this come?
This is caused by the ParamsWrapper module https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/params_wrapper.rb
This is enabled by default in your rails app by the initializer config/wrap_parameters.rb
I have the following:
#request.env['RAW_POST_DATA'] = data
#request.env['CONTENT_TYPE'] = 'application/xml'
#request.env['HTTP_CONTENT_TYPE'] = 'application/xml'
post "create", :api_key => api_key, :format => "xml"
and test.log shows this:
Processing ****Controller#create to xml (for 0.0.0.0 at 2011-07-08 15:40:20) [POST]
Parameters: {"format"=>"xml", "action"=>"create", "api_key"=>"the_hatter_wants_to_have_tea1", "controller"=>"****"}
Which... I guess is fine, but the RAW_POST_DATA doesn't show up as a hash in the parameters list in the log.... now... it works when I call the action from the terminal using curl:
curl -H 'Content-Type: application/xml' -d '<object><name>Das Object</name></object>' http://notAvailableDuringTesting.butWorksInDevelopmentMode.dev/object.xml?api_key=the_hatter_wants_to_have_tea1
what am I doing wrong here?
Is there a reason why you aren't just passing the params to the post call itself? eg:
post "create", :api_key => api_key, :format => "xml", :params => data
Controller tests are there to test that the controller action does what it expects when you send it params that you already know of. they generally aren't there to test parsing of xml into params.
If you just want to test the former - then don't bother making them into xml - just pass them as a hash.
If you really do want to test parsing of xml into params - you may need to look into something that operates outside of the scope of rails tests (eg selenium or watir)