Testing Rails jBuilder JSON APIs With RSpec

When I first embarked on creating an API for a project, I couldn’t find a ton of resources on how to test it with RSpec, so I decided to write a blog post about it.

Let’s say we’re building an app that allows users to list their second bedrooms to short-term guests. We all love Airbnb :)

This would be an application with many models that have complex associations, but let’s focus on what a host can do with their listing(s). They can create a new one, edit it, and delete it. A user can view one listing, or many listings (given certain filters like price, listing type, availability, city, etc). Let’s focus on the GET /listings, POST /listings, and DELETE /listings/:id requests that our API would support.

Here’s the listings controller. This controller ultimately inherits from ActionController::Base, and I’m using jbuilder to format the rendered JSON. Pretty straightforward.

The Tests

The first thing we need to do is tell each test to render_views, as we’re rendering JSON via jbuilder, and these are handled by Rails as views. This should go at the top before any tests.

1
2
3
4
5
6
RSpec.describe ListingsController, :type => :controller do
  render_views

  let(:json) { JSON.parse(response.body) }

  # describe...

GET

The GET /listings request should return all of the listings, with or without a filter. A simple test for this needs to:

  • make a GET request to the index method and specify the format we’re responding with (json)
  • parse the response body into a JSON object so we can see what we’re getting back from the request (which I’ve done once, above, in a let block, because this is a repeated action)
  • body looks like this:
1
2
3
4
5
6
 [{"id"=>1,
   "listing_type"=>"private room",
   "title"=>"Beautiful Apartment on Main Street",
   "price"=>"50.0",
 ....
   {"id"=>2...}]
  • grab some of that JSON data to assert an expectation on
  • test that the data is what we expect it to be (against some data in our test database, in this case I just built this in a before(:each) block in my rails_helper)

Here we go:

1
2
3
4
5
6
7
8
9
10
describe "GET /listings.json" do
  before do
    get :index, format: :json
  end

  context 'all listings' do
    it 'returns the listings' do
      expect(json.collect{|l| l["title"]}).to include(@listing1.title)
    end
  end

Next let’s test that we can return all the listings of a certain price, price being a parameter. In my listings controller, there’s an index method with a conditional to check for a params[:price], and if that was given, return all listings where “price” is that price. Our test for this will look very much like the above test, except we’re accounting for price, which is passed into a get :index method along with format.

1
2
3
4
5
6
7
8
9
context 'all listings given price filter' do
  before do
    get :index, format: :json, price: 50.0
  end

  it 'returns the listings' do
    expect(json.collect{|l| l["title"]}).to include(@listing1.title)
  end
end

POST

Now let’s work on testing our POST /listings.json request. We’re going to test that it can work (creating a new listing object in our database) and that it can fail (returning a 422).

Our success test will function much like our test for our above get requests. Instead it will call the post :create method which specifies the format as well as the required params for :listing, which we specified in a private method listing_params in our controller.

1
2
3
4
5
6
7
8
9
10
describe 'POST /listings.json' do
  context 'new listing success' do
    before do
      post :create, format: :json, :listing => {address: "123 Testing Lane", listing_type: "shared room", title: "Testing Listing", description: "idk", price: 1000.00, neighborhood_id: 1, host_id: 1}
    end

    it 'creates a new listing' do
      expect(Listing.last.address).to eq("123 Testing Lane")
    end
  end

Let’s also test that a failure will happen without the expected params (setting neighborhood_id to nil). There’s a validation on the listing model where a listing must have an associated neighborhood, which I’ve already tested in a model test. However, I wanted to test that the controller would return a 422 if the validation doesn’t pass. Here we go:

1
2
3
4
5
6
7
8
9
context 'new listing failure' do
  before do
    post :create, format: :json, :listing => {address: "123 Testing Lane", listing_type: "shared room", title: "Testing Listing", description: "idk", price: 1000.00, neighborhood_id: nil, host_id: 1}
  end

  it 'responds with 422' do
    expect(response.status).to eq(422)
  end
end

DELETE

Last, let’s test deleting a listing. We want to call the delete :destroy method with the format and the id params specified (we’re going to delete the first item in our test database). We want to test that the controller destroy method deletes the record from the database.

1
2
3
4
5
6
7
8
9
10
11
describe 'DELETE /listings/:id.json' do
  before do
    delete :destroy, format: :json, id: 1
  end

  context 'delete a listing' do
    it 'deletes a listing' do
      expect(Listing.where(id: 1)).to be_empty
    end
  end
end

There we go! RSpec makes it easy to test Rails APIs by simulating HTTP requests via get, post, patch, and delete methods. Below are some gists of the code in full discussed in this post.

Code

Comments