Rails functional test: sending URL query parameters in POST request - ruby-on-rails

I'm sending a POST request in a Rails functional test like this:
post :create, collection: { name: 'New Collection' }
collection gets sent as JSON-encoded form data, as expected.
What I can't figure out is how to add a query to the URL. The documentation says that I can access the request object and modify it before it gets sent. So I tried this:
#request.GET[:api_key] = 'my key'
post :create, collection: { name: 'New Collection' }
But, :api_key never appears in the request.GET hash on the server. (It does when I send it though another HTTP client, though.)

A little background first to clarify things: although a request cannot be both GET and POST at the same time, there is nothing stopping you from using both the query string and body form data when using POST. You can even have a POST with all parameters in the query string and an empty body, though this sounds quite unusual.
Rails supports this scenario and indeed you can easily send a form using a POST request and still have query in the form's action. The query will be accessible with request.GET hash (which is an alias of query_string), while the POST body params with the request.POST hash (an alias of request_parameters). The params hash is actually constructed from the combined GET and POST hashes.
However, from my research it seems that Rails does not support passing query string in POST requests in functional controller tests. Although I could not find anything regarding this in any documentation or among known issues on github, the source code is quite clear. In the following text, I'm assuming that you use Rails 4.
Why it does not work
The problem with functional controller tests is that they don't use real requests / responses but they simulate the HTTP handshake: the request is mocked up, its parameters filled in appropriate places and the given controller action is simply called as a normal ruby method. All of this is done in the action_controller/test_case classes.
As it turns out, this simulation is not working in your particular case, due to two reasons:
The parameters passed in when running the test are always handed over either to the request_parameters, i.e. the request.POST hash when using a post request or to the query_string (i.e. request.GET) for get test requests. There is no way for both of these hashes to be set during a single test run.
This actually makes some sense as the get, post, etc. helpers in functional tests accept only a single hash of params so the internal test code cannot know how to separate them into the two hashes.
It is true that one can set up the request before running the test using the #request variable, but only to a certain extent, you can set headers, for example. But you cannot set internal attributes of the request, because they are recycled during the test run. The recycling is done here and it resets all internal variables of the request object and the underlying rack request object. So if you try to set up the request GET parameters like this #request.GET[:api_key] = 'my key', it won't have any effect as the internal variables representing this hash will get wiped during recycling.
Solutions / workarounds
Give up functional testing and choose integration tests instead. Integration tests allow to set the rack environment variables separately from the main parameters. The following integration test passes the QUERY_STRING rack env variable besides the normal post body params and should work flawlessly:
class CollectionsTest < ActionDispatch::IntegrationTest
test 'foo' do
post collections_path, { collection: { name: 'New Collection' } },
{ "QUERY_STRING" => "api_key=my_api_key" }
# this proves that the parameters are recognized separately in the controller
# (you can test this in you controller as well as here in the test):
puts request.POST.inspect
# => {"collection"=>{"name"=>"New Collection"}}
puts request.GET.inspect
# => {"api_key"=>"my_api_key"}
end
end
You can still use most of the features from functional tests in your integration tests. E.g. you can test for assigned instance variables in the controller with the assigns hash.
The transition argument is supported also by the fact that Rails 5 will deprecate functional controller tests in favor of integration testing and since Rails 5.1 these functional tests support will be moved out to a separate gem.
Try Rails 5: although functional tests will be deprecated, its source code seems to have been heavily rewritten in the rails master and e.g. recycling of the request is not used any more. So you might give it a try and try to set the internal variables of the request during test setup. I have not tested it though.
Of course, you can always try to monkey-patch the functional test so that it supports separate params for the query_string and request_parameters hashes to be defined in tests.
I'd go the integration tests route :).

I assume that the controller is named CollectionsController, and its route to create action is /collections (if not, you just have to adapt the example bellow)
And I also assume you are in a request spec
This should work:
post '/collections?api_key=my_key', collection: { name: 'New Collection' }

The 2nd argument to post is a hash of all the params you'll receive in the controller. Just do this:
post :create, collection: { name: 'New Collection' }, more_params: 'stuff', and_so_on: 'things'
Those params will be available in the controller:
params[:and_so_on] == 'things'

You want to send a POST request:
I'm sending a POST request in a Rails functional test like this:
But you want to retrieve data from a GET request:
But, :api_key never appears in the request.GET hash on the server.
A request cannot be GET and POST at the same time, if you are sending a POST request and pass parameters in the query string then you would have those parameter values available on a POST request, GET just won't have anything.
Then:
#request.GET[:api_key] = 'my key'
post :create, collection: { name: 'New Collection' }
You are modifying the GET values on the request, but then you actually send a POST request which means that when the post method gets called and the request is sent to the server only what you sent on the POST will be available. Just send the api key bundled with the POST request (could be inside the collection hash for that matter)

This is also a problem when testing POST actions with RSpec (v3.4).
A workaround is to mock the return value of request.GET or request.query_string methods.
it "should recognise a query parameter in post action" do
allow(subject.request).to receive(:query_string).and_return("api_key=my%20key")
#params = {collection: { name: 'New Collection' }}
expect(subject.request.query_string).to eq "api_key=my%20key"
post :create, #params
end

Related

How to write a good request spec for #index?

I am new to TDD and really enjoy it. I am using RSpec.
I am trying to learn to write good request specs (in general) and can find very little written on how to test the index method.
I have found this article: https://medium.com/#lcriswell/rails-api-request-specs-with-rspec-effeac468c4e, but I am not interested in testing an API, but an application with views.
What should I include on my index request tests and why?
The first spec in the article is great, you can use it in testing a regular controller for responses.
If you are using any sort of authorization(e.g cancancan), you can test the same request for multiple types of users and check if you get a redirect or a success(you might have to mock the sign-in).
For testing the views that are being rendered, you can try this:
it { is_expected.to render_template(:index) }
If your action assigns instance variables, you can test out that the variable is a certain value like so:
expect(assigns(:foo)).to be true
If your action responds to different formats(HTML, json, ...), you can write different contexts for each of the formats, each time by changing the request(hint: for JS in your specs submit your request like so: get :index, xhr: true)

How to assert url was called from javascript using rspec and capybara

Scenario:
We use capybara integration tests to test that our frontend plumbing (javascript) is connected properly.
Sometimes all we need to validate the test is:
has content rendered properly on the page
has the js called the correct url open interaction
Problem:
Item 1 above is easy. However, with item 2 I can't seem to find an easy way to say:
Assert that url was called from js in browser.
Example:
it "should call coorect url with correct query string" do
visit widgets_path
# THIS IS WHAT I NEED TO KNOW
expect(something).to receive(:some_method).with(url: "my/test/url", params: {per_page: 2})
# In other words, I don't want the controller action to run. I don't care about the result since the controller is being tested elsewhere.
# I just need to know that the correct URL was called with the correct params.
within 'ul.pagination' do
click_on '2'
end
end
I've tried mocking the controller action, but there's no way to inspect the params. Or is there? Without inspecting the params, how can I know if the correct stuff was sent? All I know it's the correct route, which isn't enough.
If I could inspect the params then this would be solved... but otherwise?
If you are looking for the Rails solution, here it is! Tested with Rails 5.1.3.
1) Create a request params matcher spec/support/matchers/request_with_params.rb
RSpec::Matchers.define :request_with_params do |params|
match { |request| request.params.symbolize_keys.slice(*params.keys) == params }
end
2) Create a helper method for your acceptance tests (you can use some logics to pass symbol instead of class UsersController -> :users if needed)
def expect_request(controller, action, params = {})
expect_any_instance_of(ActionDispatch::Routing::RouteSet::Dispatcher)
.to receive(:dispatch)
.with(controller, action.to_s, request_with_params(params), anything)
end
end
3) Use it!
expect_request(UsersController, :index)
or with params
expect_request(UsersController, :show, { id: 1 })
OR
4) There is another way in using https://github.com/oesmith/puffing-billy Check this gem for intercepting requests sent by your browser. But it can be an overkill if you need to mock only certain requests to your backend app.
Capybara integration tests intentionally don't support that. They are end-to-end blackbox tests, shouldn't generally be mocked, and really only support checking for things visible to the user in the browser. In your example case that would mean expecting on whatever visible change is caused by the JS call to the specific URL. Something like
expect(page).to have_css('div.widget', count: 2)

How do I post json data in an rspec feature test powered by Capybara

Short question
I need to post JSON in feature tests, something like in my controller tests:
post '/orders.json', params.to_json, format: :json
But I need to do it in feature tests, and page.driver.post only posts form data. How do I post JSON in Capybara?
Long Explanation
I have a (non-Rails) app (let's call it the planet) that posts JSON to my rails app (call it a star) to create a record, and then forwards the user to the url for the show action.
I'm creating a feature spec, but since the first interaction isn't part of the Rails, app, I need to mock it using JSON.
I've been using this:
page.driver.post '/orders.json', params.to_json
But this seems to post as a form, whereas my planet application posts JSON. This leads to some really funky parameter problems, where parsing JSON gives me different params from form-data.
How do I post JSON in Capybara?
TL;DR - you don't
Capybara is designed to emulate a user for feature tests. Hence why the post method is driver specific (page.driver.xxx) and really isn't intended for use directly by tests. A user can't just submit a POST without a page to submit it from. Therefore, if you do actually need to test this via feature tests, the best solution is to create a small test app that provides a page you can have Capybara visit which will automatically (or on button click, etc) have the browser make the AJAX post to the app under test and handle the response.
So, turns out that it just isn't possible with Capybara. See Thomas Walpole's answer for more details.
As a workaround, I used httparty:
require 'httparty'
RSpec.feature 'Checkouts', type: :feature do
include HTTParty
base_uri 'http://localhost:3000'
private
def checkout_with(cart)
post orders_path(format: :json).to_s,
body: cart.to_json,
headers: { 'Content-Type' => 'application/json' }
end
end

Rspec: How to use expect to receive with a resource which does not exist yet?

In an action called via a post request I'm creating a resource call RequestOffer and send an email with ActionMailer using the created resource as a parameter:
#request_offer = RequestOffer.new(request_offer_params)
if #request_offer.save
RequestOfferMailer.email_team(#request_offer).deliver_later
end
When my controller spec, I want to test that my RequestOfferMailer is called using the method email_team with the resource #request_offer as a parameter.
When I want to user expect(XXX).to receive(YYY).with(ZZZ), the only way I found was to declare my expectation before making the POST request. However, ZZZ is created by this POST request, so I have no way to set my expectation before.
# Set expectation first
message_delivery = instance_double(ActionMailer::MessageDelivery)
# ZZZ used in .with() does not exist yet, so it won't work
expect(RequestOfferMailer).to receive(:email_team).with(ZZZ).and_return(message_delivery)
expect(message_delivery).to receive(:deliver_later)
# Make POST request that will create ZZZ
post :create, params
Any idea how to solve this problem?
If this is a functional test then I would isolate the controller test from the DB. You can do this by using instance_doubles and let statements. Here's an example that you may like to extend for your purposes
describe '/request_offers [POST]' do
let(:request_offer) { instance_double(RequestOffer, save: true) }
before do
allow(RequestOffer).to receive(:new).
with(...params...).
and_return(request_offer)
end
it 'should instantiate a RequestOffer with the params' do
expect(RequestOffer).to receive(:new).
with(...params...).
and_return(request_offer)
post '/request_offers', {...}
end
it 'should email the request offer via RequestOfferMailer' do
mailer = instance_double(ActionMailer::MessageDelivery)
expect(RequestOfferMailer).to receive(:email_team).
with(request_offer).and_return(mailer)
post '/request_offers', {...}
end
end
The key to this is using 'let' to declare an instance double of the model that you intend to create. By setting expectations on the class you can inject your instance double into the test and isolate from the DB. Note that the 'allow' call in the before block is there to serve the later specs that set expectations on the mailer object; the 'expect' call in the first test will still be able to make assertions about the call.
Would it be enough to make sure the argument is an instance of RequestOffer? Then you could use the instance_of matcher. For example:
expect(RequestOfferMailer).to receive(:email_team).with(instance_of(RequestOffer)).and_return(message_delivery)
I found this option in the Rspec 3.0 docs: https://relishapp.com/rspec/rspec-mocks/v/3-0/docs/setting-constraints/matching-arguments
The last argument of the with method is a block. You can open up the arguments and do anything you like there.
expect(RequestOfferMailer)
.to receive(:email_team)
.with(instance_of(RequestOffer)) do |request_offer|
expect(request_offer.total).to eq(100) # As one example of what you can to in this block
end.and_return(message_delivery)
You can also set the instance_of matcher to be anything if you're not even sure what object type you're expecting.

What is the best way to make a POST request from cucumber to create a new object?

For several of my cuke scenarios, I want to POST the user to a page with an object in the params, so that Rails will make a new object in the database. Is there a convenient way to send a POST+params to an action (either using something in cucumber or rails) or do I have to write my own function using Net::Http?
Just visit the controller you want to post some parameters to in order to create an object.
You do not need a form to do this. Just post the values to whatever controller is handling these posts and continue on with your testing. All the controller cares about is getting params to work with, however they get posted.
visit "/user", :post, :firstName => "Foo", :lastName => "Bar"
As detailed in the webrat documentation:
visit (url = nil, http_method = :get, data = {})
UPDATE: Unless I'm mistaken Capybara has more or less superseded Webrat and generally the there's an opinion that cucumber level tests should exercise the actual user interface, and not the API that interface interacts with. That said, it is still possible and I still find it useful myself when early on in a project I just want to test my controllers and figure out the UI later.
So in Capybara, depending on your driver, but assuming rack/test:
page.driver.post('/user', { firstName: "Foo", lastName: "Bar" })
It sounds like you don't have a form for this action? If you do have a form, you just complete the interaction via the web UI (using the tool of your choice ... webrat etc etc)
Otherwise, if this is to setup some test data, it's probably better to create the object directly in a step.
Given that an object exists
Do ...
Under the "Given" step, create the necessary object too meet the assumption in the rest of the test steps.

Resources