This one has bitten me twice now so I'm writing about it hoping I won't need to discover the solution the hard way again.
You'll see examples of JSON API implementations on top of Rails in the wild all over the place. Demonstrations will show how to test them interactively with cURL and they just apparently work. But somehow the specs (or Cucumber steps in my case last night) aren't passing. When a binding.pry is put in it can be observed that the params has a single key with JSON and a nil value. The CONTENTTYPE is application/x-www-form-urlencoded. All of this even when using the excellent jsonspec gem!
What was missing was that I was using the post helper with string data instead of a hash. Let me illustrate.
# features/api/authentication.feature Scenario: Authenticate Given a user account exists identified by email "a_user@example.com" and password "a_password" And I post JSON to the authenticate API endpoint with: """ {"user": { "email":"a_user@example.com", "password":"a_password" }} """ Then the JSON at "success" should be "true"
If your customers are other developers its perfectly acceptable to behavior test an API in this way. Moving on...
Here's the relevant step for the "And I post JSON...":
# features/steps/authentication_steps.rb ⋮ Given /^I post JSON to the (.*?) API endpoint with:$/ do |path, string| post api_path_to(path), string end ⋮
That won't work! Its because transmitting a string for the second parameter of post causes the CONTENT_TYPE of the request to be application/x-www-form-urlencoded. The correct implementation is to use a hash for the second parameter, like so:
# features/steps/authentication_steps.rb ⋮ Given /^I post JSON to the (.*?) API endpoint with:$/ do |path, string| json = JSON.parse(string) post api_path_to(path), json # sweet, sweet hashness end ⋮
Ah victory.
Still working on sorting out the "Then the JSON at ..." step however.
Update
The other sane approach to this - and not partially compatible with the first approach - is:
# features/api/authentication.feature Scenario: Authenticate Given a user account exists identified by email "a_user@example.com" and password "a_password" And I send and accept JSON And I post JSON to the authenticate API endpoint with: """ {"user": { "email":"a_user@example.com", "password":"a_password" }} """ Then the JSON at "success" should be "true"
The "And I send and accept JSON" sets the stage for... sending and accepting JSON!
# features/step_definitions/api_steps.rb Given /^I send and accept JSON$/ do header 'Accept', 'application/json, text/javascript, *' header 'Content-Type', 'application/json' end
And now we can post the raw string rather than the parsed hash:
# features/step_definitions/authentication_steps.rb Given /^I post JSON to the (.*?) API endpoint with:$/ do |path, string| post api_path_to(path), string end
Now onto solving "Then the JSON at..." step. This I had a bit of difficulty with because I was trying to use Capybara to test the API but I was mixing it with the post helper method which has absolutely no relationship with Capybara at all. No wonder I was receiving blank pages! And even though json_spec is demonstrated using Capybara that's not going to get you very far. So instead of:
# from json_spec article def last_json page.source end
I instead use:
# features/support/json_spec_env.rb require "json_spec/cucumber" def last_json # don't mix Capybara and Rack::Test for testing JSON APIs! last_response.body end
And this lets me use json_spec's excellent matchers for verifying my JSON.
The apipathto method is a convenience helper method for an indirection layer to map human readable resource names to their correct API endpoint:
# features/support/paths.rb module NavigationHelpers ⋮ def api_path_to(endpoint_name, auth_token = nil) case endpoint_name when /authenticate/ path = '/api/sessions/authenticate' when /resource/ path = '/api/resources' when /new user/ path = '/api/users' end path += "?auth_token=#{auth_token}" unless auth_token.nil? path end ⋮ end World(NavigationHelpers)