Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use RSpec with JBuilder?

I'm looking for a clean way to use JBuilder and test the json output with RSpec. The popular way for JSON testing is to implement the as_json method, and then in RSpec compare the received object with the object.to_json method. But a large reason I'm using JBuilder is that I don't want all the attributes that to_json spits out; so this breaks comparison.

Currently with JBuilder I'm having to do the following to test the RSpec results:

1) Create a Factory object: @venue

2) Create a hash inside my RSpec test that contains the "expected" JSON string back

@expected => {:id => @venue.id,:name=>@venue.name..........}

2) Compare the @expected string to the results.response.body that is returned from the JSON call.

This seems simple, except I have objects being rendered with 15+ attributes, and building the @expected hash string is cumbersome and very brittle. Is there a better way to do this?

like image 705
beeudoublez Avatar asked Mar 30 '12 15:03

beeudoublez


3 Answers

It's a little clunkier than with say ERB, but you can use binding and eval to run the Jbuilder template directly. E.g. given a typical Jbuilder template app/views/items/_item.json.jbuilder that refers to an instance item of the Item model:

json.extract! item, :id, :name, :active, :created_at, :updated_at
json.url item_url(item, format: :json)

Say you have an endpoint that returns a single Item object. In your request spec, you hit that endpoint:

get item_url(id: 1), as: :json
expect(response).to be_successful # just to be sure

To get the expected value, you can evaluate the template as follows:

item = Item.find(1)                          # local variable `item` needed by template
json = JbuilderTemplate.new(JbuilderHandler) # local variable `json`, ditto

template_path = 'app/views/items/_item.json.jbuilder'
binding.eval(File.read(template_path))       # run the template
# or, for better error messages:
#   binding.eval(File.read(template_path), File.basename(template_path))

expected_json = json.target!                 # template result, as a string

Then you can compare the template output to your raw HTTP response:

expect(response.body).to eq(expected_json)   # plain string comparison

Or, of course, you can parse and compare the parsed results:

actual_value = JSON.parse(response.body)
expected_value = JSON.parse(expected_json)
expect(actual_value).to eq(expected_value)

If you're going to be doing this a lot -- or if, for instance, you want to be able to compare the template result against individual elements of a returned JSON array, you might want to extract a method:

def template_result(template_path, bind)
  json = JbuilderTemplate.new(JbuilderHandler)

  # `bind` is passed in and doesn't include locals declared here,
  # so we need to add `json` explicitly
  bind.local_variable_set(:json, json)

  bind.eval(File.read(template_path), File.basename(template_path))
  JSON.parse(json.target!)
end

You can then do things like:

it 'sorts by name by default' do
  get items_url, as: :json
  expect(response).to be_successful
  parsed_response = JSON.parse(response.body)
  expect(parsed_response.size).to eq(Item.count)

  expected_items = Item.order(:name)
  expected_items.each_with_index do |item, i| # item is used by `binding`
    expected_json = template_result('app/views/items/_item.json.jbuilder', binding)
    expect(parsed_response[i]).to eq(expected_json)
  end
end
like image 115
David Moles Avatar answered Oct 10 '22 14:10

David Moles


You should be able to test your Jbuilder views with RSpec views specs. You can see the docs at https://www.relishapp.com/rspec/rspec-rails/v/2-13/docs/view-specs/view-spec.

An example spec for a file located at 'app/views/api/users/_user.json.jbuilder', could be something like this (spec/views/api/users/_user.json.jbuilder_spec.rb):

require 'spec_helper'

describe 'user rendering' do
  let(:current_user) { User.new(id: 1, email: '[email protected]') }

  before do
    view.stub(:current_user).and_return(current_user)
  end

  it 'does something' do
    render 'api/users/user', user: current_user

    expect(rendered).to match('[email protected]')
  end
end
like image 8
Oriol Gual Avatar answered Oct 17 '22 12:10

Oriol Gual


I don't like testing the JSON API through the views, because you have to essentially mimic, in the test, the setup already done in the controller. Also, bypassing the controller, you aren't really testing the API.

In controller tests, however, you'll find that you don't get any JSON returned in the response body. The response body is empty. This is because RSpec disables view rendering in controller tests. (For better or worse.)

In order to have an RSpec controller test of your view rendered JSON API, you must add the render_views directive at the top of your test. See this blog post (not mine), for more detailed information about using RSpec controller tests with Jbuilder.

Also, see this answer.

like image 6
Douglas Lovell Avatar answered Oct 17 '22 12:10

Douglas Lovell