3
\$\begingroup\$

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.

def json_response_body(&block)
  JSON.parse(response.body, symbolize_names: true).tap do |content|
    yield content if block_given?
  end
end

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.

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
end

This 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
end

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:

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_body
  assert_response :created
end

And it's controller action:

  # app/controllers/api/v1/users_controller.rb
  def create
    @user = User.new
    @user.assign_attributes permitted_attributes(@user)
    if @user.save
      render :create, status: 201, location: [:api, @user]
    else
      render json: { errors: @user.errors }, status: 422
    end
  end

I'm using Jbuilder to render the actual JSON.

# app/views/api/v1/users/create.json.jbuilder
json.(@user, :id, :name, :email, :created_at)
json.auth_token @user.auth_token
\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

I think you're right to be suspicious of your test setup. Given your code, I imagine you're using #to_json/#as_json in your controllers already, which in effect reduces your test to:

assert_equal record.as_json, record.as_json

which 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.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks. In fact, I am not using to_json or as_json. Not directly at least. I'm using Jbuilder and manually constructing my JSON with a combination of templates and partials. Sorry, it would have been helpful to include this in the question, and I did. So it's no longer as deterministic as assert_equal record.as_json, record.as_json. I'm not sure how much this changes your assertion, but it seems like my approach is the poor man's version of the gem. Going through the to_json helps me construct my expected response, which guarantees only the exact fields are included. \$\endgroup\$
    – Mohamad
    Commented Feb 8, 2016 at 10:05
  • \$\begingroup\$ @Mohamad Gotcha. I thought your suspicions stemmed from using to_json/as_json in both controller and test - which, yeah, that'd be redundant. Testing jbuilder output against to_json makes more sense – but at the same time, if the expected output corresponds to to_json, you might as well use to_json to construct that output and skip jbuilder. Of course, jbuilder can do things to_json can't, which is why it's useful, but it also means you can't test any jbuilder output against to_json. Hence why something like that gem is really handy \$\endgroup\$
    – Flambino
    Commented Feb 8, 2016 at 14:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.