RSpec approach to test XML and HTTP responses? - ruby-on-rails

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.

Related

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

Same spec, multiple assertions, readable output

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?

RSpec mocking response attribute

I'm stuck with a small issue which I can't seem to find on the RSpec Doc (or elsewhere). I suspect it's probably to do because it's trying an HTTP call but unsure.
I'm currently attempting to mock a response in my controller, the actual response is coming straight from aws-sdk (so it's an external service).
Controller:
response = {
snapshot_id: client.copy_snapshot({foo, bar}).snapshot_id
}
Is it possible to stub .snapshot_id?
_spec.rb
before do
allow(client).to receive(:copy_snapshot).and_return('Something')
put :update, format: :json
puts JSON.parse(response.body)
end
The tests passes successfully without adding .snapshot_id on the controller page and I can see
{"snapshot_id"=>"Something"}
Otherwise I get "this resource is currently unavailable." adding it in, I'm slightly lost if I'm doing it wrong or if this is intended behavior.
The reasoning is that when the server is on I will actually get a response back, however omitting snapshot_id causes the server to hang as amazon returns back a lot of data (headers, etc)
Any help is greatly appreciated.

RSpec / FactoryGirl - How to pass params in post request spec

I have a Rails API backend set up with restful routes. I'm trying to pass in parameters for a post request spec, but I've run into some troubles.
In my app a user has many timelines, and a timeline has many entries.
I can correctly create a timeline with a post request by doing the following:
post '/timelines', '{ "timeline": { "title":"My timeline", "body":"My timeline body"}}', #token_header
The problem i have is when i have to pass more than just string, i.e. an image. My entry model has an image attribute, but I can't figure out how to pass in into the parameters.
I have the following test which illustrates what i want to do. As you can see I've declared some entry_attributes but I can't figure out how to correctly pass them as I did with the timeline params.:
context 'POST /timelines/:id/entries' do
it 'creates an entry for the given timeline' do
entry_attr = FactoryGirl.attributes_for(:entry)
post "/timelines/#{#timeline.id}/entries", {}, #token_header
expect(response.status).to eq 201
end
end
Any help is greatly appreciated...

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.

Resources