Same spec, multiple assertions, readable output - ruby-on-rails

I am writing some request specs for an API now. I see myself doing a lot of assertions about the same kind of request. This includes (but is not limited to)
Responding with a particular HTTP status code
Changing the state of one or more ActiveRecord objects.
Responding with a particular JSON data structure
Publishing a particular event to Rabbit
Currently, I am writing four it blocks - one for each assertion. Each of my it blocks now contain a post call, followed by an assertion. As a result, these four block contain duplicate method calls (I know I could wrap this in a before), but worse: it hits the app 4 times.
The easiest way to avoid all these duplicate calls to post and subsequent request processing would be to place the 4 assertions under the same it block. However, this sacrifices readability and produces inadequate output when running the spec suite.
Basically, I am looking for a way to write something like this:
context 'products#create' do
context 'success' do
post '/some/api/endpoint', some: 'data', headers: {access_key: 1234}
it 'responds with 200 OK' do
expect(response.status).to eq 200
end
it 'changes the database state' do
expect(some_model.reload.title).to eq 'new title'
end
it 'responds with a JSON representation of the resource' do
expect(JSON.parse(response.body)).to match hash_including(desc: 'ription', price: 1234)
end
# and so on...
end
end
I have tried putting the post request in a before(:all) block, but this stops me from using allow(SomeExternalService).to receive(:some_call).and_return('something')
Ideas for doing this?

Related

Reducing network requests for RSpec controller specs, how to set headers

I'm running a Rails 5 JSON API server. I have some controller specs written, but I' noticing that they are taking a really long time to run, and I'm looking for a way to optimize.
One way I've read about is to reduce the number of network requests by essentially making the GET/POST requests in a before(:all) or before(:context) block, and then having the subsequent it statements test the response from the single network request.
One issue I'm running into is that within my before(:context) block, I don't seem to have access to modify the request headers. For example, currently it my code looks like:
before(:each) do
add_authorization_header
get :index
end
where add_authorization_header looks like:
request.headers['Authorization'] =
"Token token=#{some_authorization_token)}"
but when I chance before(:each) to before(:context) I get the following error:
NoMethodError: undefined method `headers' for nil:NilClass
Is there a way to set the request headers within a before(:context) block?
I think you have no access to the request object inside the example group. Try:
before(:context)
headers = { "Authorization" => "Token token=#{some_authorization_token)}" }
get :index, params: {}, headers: headers
end

Rails functional test: sending URL query parameters in POST request

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

Parsing Request Headers in Test::Unit

I'm trying to parse HTTP request headers in Test::Unit, but to no avail. I'm writing a functional test for a controller like so:
test "create a shopify order" do
get :order, PARAMS, {HEADER1 => VAL, HEADER2 => VAL}
assert_response :success # Make sure this returns 200, first off
...
end
Normally, I would read the headers like, request.headers[HEADER1], but this returns nil in Test::Unit. request isn't defined.
How do I actually grab the value of the headers I set in the above request? And how do I assign them to request? My app pulls from webservices, and I need to test the values that are passed through in headers, so I don't want to change my app code. I just need to simulate what those requests are like in Test::Unit.
Thanks!
Knowing what test quite you're using certainly helps (thanks Jesse). I found that I'd been looking at the doc for integration tests, not functional tests, and that setting headers works differently in functional tests:
http://twobitlabs.com/2010/09/setting-request-headers-in-rails-functional-tests/
So I wasn't setting the headers I thought I was. They were being read just fine--just not set.

RSpec approach to test XML and HTTP responses?

I have a RESTful site that uses both the XML and web responses (API and web site). Since there are a lot of pages, my current goal is setting up RSpec to simply request each of the pages in both data formats and check if the returned response is 200. What is the best way to check for both XML and HTTP 200 response? I know I should be doing TDD upfront, but right now I need this as a shell.
Example: I want to request both "/users" and "/users.xml" and test if there weren't any server errors (200 OK)
I wrote a blog post on testing JSON APIs with RSpec a couple of weeks ago.
Basically, the way we are doing it is to get the actual response, and parse it to make sure it has the right content. As an example:
context "#index (GET /artworks.json)" do
# create 30 Artwork documents using FactoryGirl, and do a HTTP GET request on "/artworks.json"
before(:each) do
30.times { FactoryGirl.create(:artwork) }
get "/artworks.json"
end
describe "should list all artworks" do
# the request returns a variable called "response", which we can then make sure comes back as expected
it { response.should be_ok }
it { JSON.parse(response.body)["results"].should be_a_kind_of(Array) }
it { JSON.parse(response.body)["results"].length.should eq 30 }
# etc...
end
end
Obviously a simple example, but hopefully you get the idea. I hope this helps.

How can I test only a part of the URL I am redirected to (using assert_redirected_to)?

In a functional test of my Rails app, I want to test where I am redirected to. The expected URL points to an external resource (that means it's not part of my application).
The URL looks like this: https://my.url.com/foo?bar1=xyz&bar2=123
Unfortunately I can't predict the parameters, because they are generated by an external resource.*
However, the rest of the URL always stays the same: https://my.url.com/foo
I usually use assert_redirected_to for this kind of test, but this expects the whole URL, including the parameters.
Can anyone think of another way to test that redirection but only check for the first part of the URL without the parameters?
(the URL is not in the assigns Hash)
*(I make an API call to an application, which responses with the URL I shall redirect_to)
The http request commands like post, get etc. create an instance variable called #response when they are called*. #response itself contains a method called redirect_url which stores the URL you have been redirected to (in case you have really been redirected).
Therefor, I can just use a normal assert_match to compare a regular expression to #response.redirect_url:
post :my_action_to_test
assert_response :redirect
assert_match /https:\/\/my.url.com\/foo/, #response.redirect_url
*(actually, these http methods just use the private method process, which creates the #response variable).
New and improved!
Rails offers regex matching in assert_redirected_to as of 6.1.
You can just call
assert_redirected_to %r(\Ahttp://example.org/funky_cold_medina/\d+)
Rspec Rails just delegates to this method, so even though it is not documented, it works too.
subject { post some_path, params: { chunky_bacon: true} }
it "puts the lotion on itself" do
expect(subject).to redirect_to( %r(/catch_the_puppy/\d+) )
end
Two quick thoughts on this:
1) If your functional test actually connects to the external application, why not just get the params out of it as you would normally and test that the redirect occurs properly?
2) If your functional test does not actually connect to the external application, then you're faking it anyway, so I would just skip testing the redirect URL and just test for a redirect with assert_response :redirect. Or, create a mock that returns the URL for redirection as if it were the external app, but do it in such a way that you can get the params out of it.
That said, don't get so carried away with tests that you feel you have to cover every single possible scenario.
How about this? It wraps assert_redirected_to to allow a Regexp as the first argument. This won't work if you try to match the Regexp to a Hash -- only a String, though. Doing that would take a bit more work.
ActionController::TestCase.class_eval do
old_assert_redirected_to = method(:assert_redirected_to)
define_method(:assert_redirected_to) do |*args|
if args.[0].kind_of?(Regexp)
assert_response(:redirect, args[1])
assert args[0] === #response.redirected_to
else
old_assert_redirected_to.bind(self).call(*args)
end
end
end
I use the following method to ignore any query string parameters. It is based off of the official implementation of assert_redirected_to
# ignores any query string params eg. notice or alert messages for flash
def custom_assert_redirected_to(path)
assert_response :redirect
if path === Regexp
url = path
else
url = ActionController::Redirecting._compute_redirect_to_location(#request, path)
end
assert_equal url, #response.location.split("?").first
end

Resources