Farewell JSON API Gems
In the past, testing JSON APIs tended to be a bit painful for me. Most of this pain revolved around setting expectations on the response body.
If you treat the response as a raw string, attempting to use regular expressions ends up being an exercise in how you handle frustration. While a JSON body is a string, it has structure. Using regular expressions for parsing them is akin to using a hammer on a screw. It’ll get the job done, but it’s the wrong tool for the job.
Ruby gives us JSON.parse
.
Which will convert a valid JSON string into a more familiar object structure.
Now comes the “fun” part of actually verifying that structure:
- Sometimes you only care about part of the response
- Sometimes you care about validating the entire response
- Sometimes the response is very complicated consisting of many smaller, more logically meaningful, structures
- Sometimes you only care about the general structure (e.g. this value must be a number, that value must be either an empty array or an array of strings, etc.)
It is possible to do all of these validations out of the box. In my experience, writing them tended to be tedious. Often the resulting code left something to be desired in terms of readability. This was especially true when validating the general response structure.
I like to follow the “one expectation per spec” guideline. However, this lead to writing many small specs. Normally, this is perfectly fine and something I advocate you do. However, in terms of a JSON response, it means I need to have more discipline to keep everything explicitly organized.
Naturally in the Ruby community, many gems have sprouted up to help with this problem set. I’ve had a bit of success with some of those gems in the past. However, with the release of RSpec 3, several new features have eliminated my need for these JSON gems.
Expectations on a JSON response is a great fit for composing matchers. When I need to logically group checking several options, the compound matchers are the perfect tool.
Often people don’t realize that the matcher messages (i.e. exist
, be
, eq
,
include
, etc) are just factories helpers (see
endnotes). They are just helper methods which create
the matcher object for you. That means, we can easily write our own using our
app’s domain language.
Let’s jump right into an example!
These examples are assuming a JSON structure like one of the ones listed on the jsonapi.org site. Though I am assuming integer value are represented as numbers and not strings, since that is valid JSON and more meaningful:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
|
The possibilities are fairly endless. We could improve this further by allowing the factories to take model instances or attribute hashes. We can use those to check specific content when available:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
Happy RSpec’ing!
Updates 2014-09-30
Thanks to everyone who provided feedback on this post. I’ve take it all into consideration and made the following changes:
The first code sample now shows the full spec file structure. This hopefully makes the important distinction that the helper methods are not being defined on
main
.The line
require 'support/json_api_helpers'
isn’t loading another library. Instead it is loading an extracted set of shared helper methods common to nearly all JSON API request specs for this project. These have been extracted to a module to keep them off ofmain
and placed inspec/support
.This follows the new guidance that specs should only load those files which they need. It also makes it easier for your future self and your co-workers to come back to the file later and try to find where things are defined.
I’m including the file below for completeness:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
It was pointed out that
all match
will handle the emptyArray
case. It is possible to amend the abovebe_an_empty(Array).or(all_match())
to instead read:be_an(Array).and(all_match())
.This is a helper method. My reference to it as a factory was more explicitly attempting to describe it as a helper method which instantiates another object. In Rails, people often know of “factories” from the “factory vs fixture” debate. Often those factories are relatively simple wrappers around constructors. After researching this a little more it seems in the larger programming world “factory” is not the proper term. Perhaps “creator” is. I will move to calling them helpers in the future.