4
votes

As the title suggests I'm just trying to test the create action in my API controller with RSpec. The controller looks something like:

module Api
  module V1
    class BathroomController < ApplicationController
      skip_before_action :verify_authenticity_token, only: [:create]`

      def create
        bathroom = Bathroom.new(bathroom_params)
        bathroom.user = current_user
        if bathroom.save
          render json: { status: 'SUCCESS', message: 'Saved new bathroom', bathrooms: bathroom }, status: :ok
        end
      end

      private
      def bathroom_params
        params.require(:bathroom).permit(:establishment, :address, :city, :state, :zip, :gender, :key_needed, :toilet_quantity)
      end

    end
  end
end

Right now this is doing exactly what it should which is great. The test however...not so much. Here's what I have for the test portion:

describe "POST #create" do
  let!(:bath) {{
    establishment: "Fake Place",
    address: "123 Main St",
    city: "Cityton",
    state: "NY",
    zip: "11111",
    gender: "Unisex",
    key_needed: false,
    toilet_quantity: 1
  }}
  let!(:params) { {bathroom: bath} }
  it "receives bathroom data and creates a new bathroom" do
    post :create, params: params

    bathroom = Bathroom.last
    expect(bathroom.establishment).to eq "Fake Place"
  end
end

I'm sure there's more than one thing wrong here but I'm having trouble finding much information about the right way to go about testing this. Any insight or suggestions would be greatly appreciated.

3
I think there is a good chance that Bathroom.last is not returning the record you expect.Sean
However, some would argue that this is not something for a controller test. Do you have any validations that are failing? Can you post the test failure?Sean

3 Answers

9
votes

I would skip controller specs altogether. Rails 5 has pretty much delegated ActionController::TestCase (which RSpec wraps as controller specs) to the junk drawer. Controller tests don't send real http requests and stub out key parts of Rails like the router and middleware. Total depreciation and delegation to a separate gem will happen pretty soon.

Instead you want to use a request spec.

RSpec.describe "API V1 Bathrooms", type: 'request' do
  describe "POST /api/v1/bathrooms" do
    context "with valid parameters" do
      let(:valid_params) do
        {
           bathroom: {
            establishment: "Fake Place",
            address: "123 Main St",
            city: "Cityton",
            state: "NY",
            zip: "11111",
            gender: "Unisex",
            key_needed: false,
            toilet_quantity: 1
          }
        }
      end

      it "creates a new bathroom" do
        expect { post "/api/v1/bathrooms", params: valid_params }.to change(Bathroom, :count).by(+1)
        expect(response).to have_http_status :created
        expect(response.headers['Location']).to eq api_v1_bathroom_url(Bathroom.last)
      end

      it "creates a bathroom with the correct attributes" do
         post "/api/v1/bathrooms", params: valid_params
         expect(Bathroom.last).to have_attributes valid_params[:bathroom]
      end
    end

    context "with invalid parameters" do
       # testing for validation failures is just as important!
       # ...
    end
  end
end

Also sending a bunch of junk like render json: { status: 'SUCCESS', message: 'Saved new bathroom', bathrooms: bathroom }, status: :ok is an anti-pattern.

In response you should just send a 201 CREATED response with a location header which contains a url to the newly created resource or a response body that contains the newly created resource.

def create
  bathroom = current_user.bathrooms.new(bathroom_params)
  if bathroom.save
    head :created, location: api_v1_bathroom_url(bathroom)
  else
    head :unprocessable_entity
  end     
end

If your client can't tell by looking at the response code if the response is successful or not you're doing it wrong.

1
votes

You don't really need to test the values from the record saved on the database, you could do something like:

expect(post :create, params: params).to change(Bathroom, :count).by(1)

That's enough to test that the create action creates a record on the desired table.

Then you can add more specs to test that Bathroom.new receives the expected parameters (that way you know that it would have those fields when saved), or stub the bathroom object and it's save method to test the response.

If you want to test that the saved record has the right values, I think that spec belongs to the Bathroom model and not the controller (or better, an integration test).

0
votes

So I followed the advice of max but made one slight change to get it working. My final code was:

RSpec.describe "API V1 Bathrooms", type: 'request' do
  describe "POST /api/v1/bathrooms" do
    context "with valid parameters" do

      let(:valid_params) do
        {
           bathroom: {
            establishment: "Fake Place",
            address: "123 Main St",
            city: "Cityton",
            state: "NY",
            zip: "11111",
            gender: "Unisex",
            key_needed: false,
            toilet_quantity: 1
          }
        }
      end

      it "creates a new bathroom" do
        user = FactoryGirl.create(:user, email: "[email protected]")
        login_as(user, :scope => :user)
        expect { post "/api/v1/bathrooms", params: valid_params }.to change(Bathroom, :count).by(+1)
        expect(response).to have_http_status :created
        expect(response.headers['Location']).to eq api_v1_bathroom_url(Bathroom.last)
      end

      it "creates a bathroom with the correct attributes" do
        user = FactoryGirl.create(:user, email: "[email protected]")
        login_as(user, :scope => :user)
    post "/api/v1/bathrooms", params: valid_params
        expect(Bathroom.last).to have_attributes valid_params[:bathroom]
      end
    end
  end
end

The key was to use FactoryGirl to create a new user because the bathroom needs an associated user_id to be valid.