patternrubyrailsMinor
Testing a Rails API controller
Viewed 0 times
railsapitestingcontroller
Problem
I'm building a JSON API. I'm writing my controller tests so that they compare the response body to actual JSON I'm generating in my tests using ActiveRecord. I wrote two helper methods that make the process easier. The first simply parses the actual response body and returns it.
The second method returns a JSON representation of an object, and accepts options. This is what I expect the response to be. So I compare the output of this method to the actual response.
This way, I can write my tests like this:
Is this a good way of testing me controllers? The part that makes me think is where I'm generating the JSON using Rails helpers instead of explicitly writing the JSON myself, or looking for keys in the response body. For example:
But since Rails is pretty well tested, I feel like I can trust its JSON return. I'm not sure if this approach makes my tests brittle, since any change in the response body will cause the tests to fail. But maybe that's a good thing? Here's another example:
```
test "POST #create with valid attributes" do
user = create(:user)
api_authorization_header token: user.auth_token
post :create, product: attributes_for(:product)
expected = as_parsed_json Product.last, json_options
assert_equal expected, json_response_bo
def json_response_body(&block)
JSON.parse(response.body, symbolize_names: true).tap do |content|
yield content if block_given?
end
endThe second method returns a JSON representation of an object, and accepts options. This is what I expect the response to be. So I compare the output of this method to the actual response.
def as_parsed_json(object, options = {})
# Converting to JSON and reparsing ensures
# dates are in the same format as my controller responses.
JSON.parse object.to_json(options), symbolize_names: true
endThis way, I can write my tests like this:
test "GET #show" do
product = create(:product)
get :show, id: product
expected =
as_parsed_json(product, only: [:id, :title, :price_in_cents])
assert_equal expected, json_response_body
assert_response :success
endIs this a good way of testing me controllers? The part that makes me think is where I'm generating the JSON using Rails helpers instead of explicitly writing the JSON myself, or looking for keys in the response body. For example:
expected = { id: product.id, title: product.title }.to_json
# Or
assert_equal product.id, request.body[:id]But since Rails is pretty well tested, I feel like I can trust its JSON return. I'm not sure if this approach makes my tests brittle, since any change in the response body will cause the tests to fail. But maybe that's a good thing? Here's another example:
```
test "POST #create with valid attributes" do
user = create(:user)
api_authorization_header token: user.auth_token
post :create, product: attributes_for(:product)
expected = as_parsed_json Product.last, json_options
assert_equal expected, json_response_bo
Solution
I think you're right to be suspicious of your test setup. Given your code, I imagine you're using
which doesn't prove much of anything.
It can also hide bugs. For instance, say your product attributes are
A stricter test would be to say:
not pretty, but it's more comprehensive.
(Caveat: If your
But it's not totally comprehensive, though. Your response should probably also include added attributes like
You could simply add a line like:
but if you have multiple server-computed attributes to check, this gets annoying too.
You could however use your current helpers for that part. I.e. check some attributes against the ones you posted (so you're sure they made it through), and the computed ones against the
All that being said, if your JSON becomes even slightly more complex (e.g. includes associated/nested records), the tests become trickier.
Similarly, your JSON could include values you'll have to check in yet another way. Maybe the JSON response contains something that's neither a saved attribute in the record, or a param you sent. Maybe it contains some random element that changes for each request (don't know why it would, but it's possible), you can't compare it to a known value. Instead you'll have to check it in some other way.
I'm pretty happy with this gem which lets you declare a "pattern" with varying degrees of specificity for the JSON you expect. E.g. you can check that the returned
#to_json/#as_json in your controllers already, which in effect reduces your test to:assert_equal record.as_json, record.as_jsonwhich doesn't prove much of anything.
It can also hide bugs. For instance, say your product attributes are
name and an optional description, and you expect both to be saved. But your create action accidentally filters away description, so that's never set (and since it's optional, there's no validation error). But you're comparing the created record to itself, you'll never know that the description got lost on the way.A stricter test would be to say:
attrs = attributes_for(:product)
assert_equal attrs, json_response_body.slice(*attrs.keys)not pretty, but it's more comprehensive.
(Caveat: If your
attributes_for method has some random or sequenced elements, you have to do some boring busywork to make sure it's only called once so you can use it for comparison purposes.)But it's not totally comprehensive, though. Your response should probably also include added attributes like
id, which aren't among the ones you specified in the request.You could simply add a line like:
assert_equal Product.last.id, json_response_body[:id]but if you have multiple server-computed attributes to check, this gets annoying too.
You could however use your current helpers for that part. I.e. check some attributes against the ones you posted (so you're sure they made it through), and the computed ones against the
Product.last record. That'll help guarantee that the response JSON contains everything you expect it to contain.All that being said, if your JSON becomes even slightly more complex (e.g. includes associated/nested records), the tests become trickier.
Similarly, your JSON could include values you'll have to check in yet another way. Maybe the JSON response contains something that's neither a saved attribute in the record, or a param you sent. Maybe it contains some random element that changes for each request (don't know why it would, but it's possible), you can't compare it to a known value. Instead you'll have to check it in some other way.
I'm pretty happy with this gem which lets you declare a "pattern" with varying degrees of specificity for the JSON you expect. E.g. you can check that the returned
name is the specific string you expect, while id is simply an integer since its exact value isn't important but its type and presence are. Give it a look.Code Snippets
assert_equal record.as_json, record.as_jsonattrs = attributes_for(:product)
assert_equal attrs, json_response_body.slice(*attrs.keys)assert_equal Product.last.id, json_response_body[:id]Context
StackExchange Code Review Q#119185, answer score: 2
Revisions (0)
No revisions yet.