1

I have been tearing my hair trying to make the test to pass. I have a JSON API that looks like this:

{
  "data": [
    {
      "id": "b99f8173-0492-457f-9de9-6c1d8d6832ed",
      "type": "manufacturer_organizations",
      "attributes": {
        "account_number": "random test 123"
      },
      "relationships": {
        "organization": {
          "data": {
            "id": "fb20ddc9-a3ee-47c3-bdd2-f710541ff89c",
            "type": "organizations"
          }
        },
        "manufacturer": {
          "data": {
            "id": "1",
            "type": "manufacturers"
          }
        }
      }
    },...

I am trying to make a post :create test in rails.

let!(:manufacturer_organization) {FactoryGirl.create(:manufacturer_organization)}
let(:manufacturer_organization2) { FactoryGirl.create(:manufacturer_organization)}

...

  describe "POST create" do
    it "posts a valid manufacturer organization data" do
      authenticate
      organization = FactoryGirl.create(:organization)
      manufacturer = FactoryGirl.create(:manufacturer)

      post :create, manufacturer_organization2.to_json #what should I put here instead?

      expect(json['data'].length).to eq(2)
    end

  #=> error: JSON::ParserError: A JSON text must at least contain two octets!

I have tried various SO posts (this), this, and this article

Here are some of the attempts I have tried:

post :create, params: {organization_id: organization.id, manufacturer: manufacturer.id, account_number: "123 test account number"}
#=> error: JSON::ParserError:
   A JSON text must at least contain two octets!

or

post :create, params: :manufacturer_organization_2
#=> 
 NoMethodError:
   undefined method `symbolize_keys' for :manufacturer_organization_2:Symbol

or

json = { :format => 'json', :manufacturer_organization => { :account_number => "foo123", :organization_id => organization.id, :manufacturer_id => manufacturer.id } }
post :create, json
#=>  NoMethodError:
   undefined method `length' for nil:NilClass 

How can I test my controller to accept manufacturer_id, organization_id, and account_number via post :create? Right now the way I test it is to count initial json['data'].length (initially 1); at the end I expect json['data'].length to be 2 after successful post :create. How can I mock creating a valid manufacturer_organization input?

Edit:

Sorry, forgot to put my json method helper:

def json
  JSON.parse(response.body)
end

Also, this pass:

  describe "POST create" do
    it "posts a valid manufacturer organization data" do
      authenticate
      organization = FactoryGirl.create(:organization)
      manufacturer = FactoryGirl.create(:manufacturer)
      post :create, {account_number: "Test account numba", organization_id: organization.id, manufacturer_id: manufacturer.id}
      expect(response).to be_success
    end

while adding expect(json['success']).to eq("Yay!") gives me this error:

JSON::ParserError: A JSON text must at least contain two octets!

Controller:

  def create
    @manufacturer_organization = ManufacturerOrganization.new(manufacturer_organization_params)
    if @manufacturer_organization.save
      puts "success!"
      render json: {success: "Yay!"}
    else
      puts "Sorry, something went wrong!"
    end
  end


def manufacturer_organization_params
    api_params.permit(:organization_id, :manufacturer_id, :account_number)
end

while @api_params ||= ActionController::Parameters.new(ActiveModelSerializers::Deserialization.jsonapi_parse(params))

Community
  • 1
  • 1
Iggy
  • 5,129
  • 12
  • 53
  • 87
  • You don't need to ever explicitly convert the params to JSON in a spec. But there are tons of other issues in the code here. The payload in your example JSON looks like JSONAPI.org. But its not the correct payload to create a resource. Also you are using `FactoryGirl.create` which will give you uniqueness validation problems. Instead of using `puts` in your controller return meaningful HTTP response codes and test for that in your spec. – max Apr 15 '17 at 18:42
  • You should edit the question and state what your controller accepts and what the outcomes you are trying to test are. That way answerers can give examples that actually work instead of fixing 1 of 10 problems with the code. – max Apr 15 '17 at 18:44
  • Thanks for the comment, @max! Do you mind elaborating how to return meaningful HTTP response codes and test for that in my spec? I also edited the question and added more code for my params. – Iggy Apr 15 '17 at 18:52

1 Answers1

1

In RSpec you never* need to explicitly format the params.

post :create, params: { foo: 'bar' }, format: :json

This will properly format the hash { foo: 'bar' } as JSON in the request body.

To create a hash which matches the JSONAPI.org structure you can create a helper:

# support/api_spec_helper.rb
module APISpecHelper
  def to_json_api(model)
    {
      data: {
        type: ActiveModel::Naming.plural(model),
        attributes: model.attributes
      }.tap do |hash| 
        hash[:id] = model.id if model.persisted?
      end
    }
  end
end

You can also use the JSONAPI-RB gem or ActiveModel::Serializers to constuct/deconstruct JSONAPI responses/params.


require 'rails_helper'
require 'api_spec_helper'

RSpec.request "Manufacturer organizations" do 

  include APISpecHelper

  describe "POST '/manufacturer_organizations'" do
    let(:valid_params) do
      to_json_api(FactoryGirl.build(:manufacturer_organization))
    end
    let(:invalid_params) do
      to_json_api(ManufacturerOrganization.new(
        foo: 'bad_value'
      ))
    end

    describe "with valid attributes" do
      it "creates a manufacturer organization" do
        expect do
          post '/manufacturer_organizations', params: valid_params, format: :json
        end.to change(ManufacturerOrganization, :count).by(+1)
      end

      it "has the correct response" do
        post '/manufacturer_organizations', params: valid_params, format: :json
        expect(response).to have_status :created
        expect(response.headers[:location]).to eq(
          manufacturer_organization_path(ManufacturerOrganization.last)
        )
      end
    end

    describe "with valid attributes" do
      it "does not create a manufacturer organization" do
        expect do
          post '/manufacturer_organizations', params: invalid_params, format: :json
        end.to_not change(ManufacturerOrganization, :count)
      end

      it "has the correct response" do
        post '/manufacturer_organizations', params: invalid_params, format: :json
        expect(response).to have_status :unproccessable_entity
      end
    end
  end
end

Returning the correct status codes.

Returning the correct response codes is pretty simple:

def create
  @resource = Resource.create(some_params)
  if @resource.save
    # you can respond by pointing at the newly created resource but with no body
    head :created, location: @resource
    # or 
    render json: @resource, 
           status: :created, 
           location: @resource
  else
    render json: { errors: @resource.errors.full_messages }, 
           status: :unprocessable_entity
  end
end

If a POST request did not include a Client-Generated ID and the requested resource has been created successfully, the server MUST return a 201 Created status code.
http://jsonapi.org/format/#crud

Other Responses
A server MAY respond with other HTTP status codes.
A server MAY include error details with error responses.

The commonly accepted practice is to use 422 - Unprocessable Entity for validation errors.

One small concern is that you should use a serializer to give the correct JSON response and also serialize the correct error objects.

max
  • 96,212
  • 14
  • 104
  • 165
  • I updated this answer to represent more of the state of the art in 2018 as much has changed since 2015. Refer to the documentation of AMS or JSONAPI-RB for details on how to construct/deconstruct JSON and how to handle parameters, – max Sep 03 '18 at 08:50