Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing Rails API controller POST with RSpec

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.

like image 447
awesome_treehouse Avatar asked Dec 14 '22 20:12

awesome_treehouse


2 Answers

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.

like image 134
max Avatar answered Dec 23 '22 05:12

max


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

like image 34
arieljuod Avatar answered Dec 23 '22 05:12

arieljuod