11

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?

beeudoublez
  • 1,222
  • 1
  • 12
  • 27
  • I have been doing exactly that. I can't think of a better way to do it. I constructed a hash and apply `.to_json` on the `@expected`. One thing I do speed this up and make it less brittle. Perhaps at the cost of accuracy? Is to build my hashes with collect and map. I guess the specific nature of building custom template with any JSON string building library requires you to write a test that matches the specifics of the work you do.. :/ – stuartc Jul 09 '12 at 09:38
  • @beeudoublez are you able to use RSpec to test jbuilder views? I'm having trouble getting rspec to pass objects to the view in order for the jbuilder handler to construct the JSON. Do you have an example of working view `*_spec.rb` file? – sorens Oct 11 '12 at 04:20

6 Answers6

8

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: 'foo@bar.com') }

  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('foo@bar.com')
  end
end
Joe Leo
  • 400
  • 5
  • 12
Oriol Gual
  • 424
  • 4
  • 7
  • Deprecation Warnings: Using `stub` from rspec-mocks' old `:should` syntax without explicitly enabling the syntax is deprecated. Use the new `:expect` syntax or explicitly enable `:should` instead. – Ri1a Dec 14 '20 at 07:42
6

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.

Community
  • 1
  • 1
Douglas Lovell
  • 1,549
  • 17
  • 27
3

I have not been able to make RSpec work with the views yet, but I am testing my JSON API via controller RSpec tests. To assist with this process, I am using the api matchers gem. This gem lets you construct RSpec tests such as:

it "should have a 200 code" do
  get :list, :format => :json
  response.should be_success
  response.body.should have_json_node( :code ).with( "200" )
  response.body.should have_json_node( :msg ).with( "parameter missing" )
end
sorens
  • 4,975
  • 3
  • 29
  • 52
1

This sounds like a good use case for RSpec view specs. Are you using JBuilder for the output of a controller in views?

For example, in spec/views/venues_spec.rb

require 'spec_helper'
describe "venues/show" do
  it "renders venue json" do
    venue = FactoryGirl.create(:venue)
    assign(:venue, venue])
    render
    expect(view).to render_template(:partial => "_venue")
    venue_hash = JSON.parse(rendered)
    venue_hash['id'].should == @venue.id
  end
end
Winfield
  • 18,985
  • 3
  • 52
  • 65
1

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
David Moles
  • 48,006
  • 27
  • 136
  • 235
0

You can call the render function directly.

This was key for me to get local variables to work.

require "rails_helper"

RSpec.describe "api/r2/meditations/_meditation", type: :view do

  it "renders json" do
    meditation = create(:meditation)
    render partial: "api/r2/meditations/meditation", locals: {meditation: meditation}

    meditation_hash = JSON.parse(rendered)

    expect(meditation_hash['slug']).to eq meditation.slug
    expect(meditation_hash['description']).to eq meditation.description
  end
end
Confused Vorlon
  • 9,659
  • 3
  • 46
  • 49