Rails & RSpec - Testing API with request specs and shared examples











up vote
2
down vote

favorite
1












I have written the following shared examples which are used in multiple request specs to test a namespaced RESTful JSON API.



Application details: Rails 4.2, RSpec 3.5, Devise for authentication and Pundit for authorization. Authorization policies are tested separarely as discussed in this post.



I would greatly appreciate your feedback and suggestions on how to improve the specs (e.g., efficiency, readability, maintainability, DRYness).



Here are the shared examples:



RSpec.shared_examples "a RESTful JSON API",
http_error_instead_of_exception: true do |controller_class:,
resource_path:,
comparable_attributes:|

def self.controller_has_action?(controller_class, action)
controller_class.action_methods.include?(action.to_s)
end

# Ensure authorization (Pundit gem) is enforced
def mock_authorization(authorized: false)
# Avoid Pundit::AuthorizationNotPerformedError when using "after_action
# :verify_authorized". Use "allow" and not "expect" as #verify_authorized is
# only called when we do not raise Pundit::NotAuthorizedError.
allow_any_instance_of(Api::V1::BaseApiController).to
receive(:verify_authorized)

expectation = expect_any_instance_of(Api::V1::BaseApiController).to
receive(:authorize)
# Simulate a "not authorized" scenario
expectation.and_raise(Pundit::NotAuthorizedError) if !authorized
end

resource_singular = resource_path.split("/").last.singularize.to_sym
resource_plural = resource_path.split("/").last.to_sym

before(:each) { login_admin }

let(:record) { FactoryGirl.create(resource_singular) }
let(:records) { FactoryGirl.create_pair(resource_singular) }
# Models that validate the presence of associated records require some
# hacking in the factory to include associations in #attributes_for
let(:valid_attributes) { FactoryGirl.attributes_for(resource_singular) }
# All factories must have a trait called :invalid
let(:invalid_attributes) do
FactoryGirl.attributes_for(resource_singular, :invalid)
end
let(:response_json) { JSON.parse(response.body) }

describe "GET #{resource_path} (#index)",
if: controller_has_action?(controller_class, :index) do
before(:each) do
# Test data is lazily created. Here we must force it to be created.
records
end

it "requires authentication" do
logout_example
get resource_path
expect(response).to require_login_api
end

it "enforces authorization" do
expect_any_instance_of(Api::V1::BaseApiController).to
receive(:policy_scope).and_call_original
get resource_path
end

it "returns a 'OK' (200) HTTP status code" do
get resource_path
expect(response).to have_http_status(200)
end

it "returns all #{resource_plural}" do
get resource_path
# When testing the User model, a user created by the Devise login helper
# increases the expected record count to 3.
expected_count = resource_singular == :user ? 3 : 2
expect(response_json.size).to eq(expected_count)
end
end


describe "GET #{resource_path}/:id (#show)",
if: controller_has_action?(controller_class, :show) do
it "requires authentication" do
logout_example
get "#{resource_path}/#{record.id}"
expect(response).to require_login_api
end

it "enforces authorization" do
mock_authorization(authorized: false)
get "#{resource_path}/#{record.id}"
expect(response).to have_http_status(403)
end

context "with a valid #{resource_singular} ID" do
before(:each) do
get "#{resource_path}/#{record.id}"
end

it "returns a 'OK' (200) HTTP status code" do
expect(response).to have_http_status(200)
end

it "returns the requested #{resource_singular}" do
expect(response_json).to include(
record.attributes.slice(comparable_attributes))
end
end

context "with an invalid #{resource_singular} ID" do
before(:each) { get "#{resource_path}/9999" }

it "returns a 'not found' (404) status code" do
expect(response).to have_http_status(404)
end
end
end


describe "POST #{resource_path} (#create)",
if: controller_has_action?(controller_class, :create) do
it "requires authentication" do
logout_example
post resource_path, { resource_singular => valid_attributes }
expect(response).to require_login_api
end

it "enforces authorization" do
mock_authorization(authorized: false)
post resource_path, { resource_singular => valid_attributes }
expect(response).to have_http_status(403)
end

context "with valid attributes" do
before(:each) do
post resource_path, { resource_singular => valid_attributes }
end

it "returns a 'created' (201) HTTP status code" do
expect(response).to have_http_status(201)
end

it "returns the created #{resource_singular}" do
expect(response_json).to include(
record.attributes.slice(comparable_attributes))
end
end

context "with invalid attributes" do
before(:each) do
post resource_path, { resource_singular => invalid_attributes }
end

it "returns a 'unprocessable entity' (422) HTTP status code" do
expect(response).to have_http_status(422)
end
end
end


describe "PATCH #{resource_path}/:id (#update)",
if: controller_has_action?(controller_class, :update) do
it "requires authentication" do
logout_example
patch "#{resource_path}/#{record.id}",
{ resource_singular => valid_attributes }
expect(response).to require_login_api
end

it "enforces authorization" do
mock_authorization(authorized: false)
patch "#{resource_path}/#{record.id}",
{ resource_singular => valid_attributes }
expect(response).to have_http_status(403)
end

context "with valid attributes" do
before(:each) do
patch "#{resource_path}/#{record.id}",
{ resource_singular => valid_attributes }
end

it "returns a 'OK' (200) HTTP status code" do
expect(response).to have_http_status(200)
end

it "returns the updated #{resource_singular}" do
record.reload
expect(response_json).to include(
valid_attributes.slice(comparable_attributes))
end
end

context "with invalid attributes" do
before(:each) do
patch "#{resource_path}/#{record.id}",
{ resource_singular => invalid_attributes }
end

it "returns an 'unprocessable entity' (422) status code" do
expect(response).to have_http_status(422)
end
end
end


describe "DELETE #{resource_path}/:id (#destroy)",
if: controller_has_action?(controller_class, :destroy) do
it "requires authentication" do
logout_example
delete "#{resource_path}/#{record.id}"
expect(response).to require_login_api
end

it "enforces authorization" do
mock_authorization(authorized: false)
delete "#{resource_path}/#{record.id}"
expect(response).to have_http_status(403)
end

it "ensures the #{resource_singular} no longer exists" do
delete "#{resource_path}/#{record.id}"
# When testing the "user" resource, Devise unexpectedly logs out
# (resulting in 401 to any further requests) after *any* user is deleted.
login_admin if resource_singular == :user
get "#{resource_path}/#{record.id}"
expect(response).to have_http_status(404)
end

it "returns a 'no content' (204) status code" do
delete "#{resource_path}/#{record.id}"
expect(response).to have_http_status(204)
end
end
end


... and this is a sample request spec:



# spec/requests/users_spec.rb
require "rails_helper"

RSpec.describe "Users API", :type => :request do
it_behaves_like "a RESTful JSON API",
controller_class: Api::V1::UsersController,
resource_path: "/api/v1/users",
comparable_attributes: [:id, :email, :first_name, :last_name]
end


Thanks in advance.










share|improve this question
















bumped to the homepage by Community 10 hours ago


This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.



















    up vote
    2
    down vote

    favorite
    1












    I have written the following shared examples which are used in multiple request specs to test a namespaced RESTful JSON API.



    Application details: Rails 4.2, RSpec 3.5, Devise for authentication and Pundit for authorization. Authorization policies are tested separarely as discussed in this post.



    I would greatly appreciate your feedback and suggestions on how to improve the specs (e.g., efficiency, readability, maintainability, DRYness).



    Here are the shared examples:



    RSpec.shared_examples "a RESTful JSON API",
    http_error_instead_of_exception: true do |controller_class:,
    resource_path:,
    comparable_attributes:|

    def self.controller_has_action?(controller_class, action)
    controller_class.action_methods.include?(action.to_s)
    end

    # Ensure authorization (Pundit gem) is enforced
    def mock_authorization(authorized: false)
    # Avoid Pundit::AuthorizationNotPerformedError when using "after_action
    # :verify_authorized". Use "allow" and not "expect" as #verify_authorized is
    # only called when we do not raise Pundit::NotAuthorizedError.
    allow_any_instance_of(Api::V1::BaseApiController).to
    receive(:verify_authorized)

    expectation = expect_any_instance_of(Api::V1::BaseApiController).to
    receive(:authorize)
    # Simulate a "not authorized" scenario
    expectation.and_raise(Pundit::NotAuthorizedError) if !authorized
    end

    resource_singular = resource_path.split("/").last.singularize.to_sym
    resource_plural = resource_path.split("/").last.to_sym

    before(:each) { login_admin }

    let(:record) { FactoryGirl.create(resource_singular) }
    let(:records) { FactoryGirl.create_pair(resource_singular) }
    # Models that validate the presence of associated records require some
    # hacking in the factory to include associations in #attributes_for
    let(:valid_attributes) { FactoryGirl.attributes_for(resource_singular) }
    # All factories must have a trait called :invalid
    let(:invalid_attributes) do
    FactoryGirl.attributes_for(resource_singular, :invalid)
    end
    let(:response_json) { JSON.parse(response.body) }

    describe "GET #{resource_path} (#index)",
    if: controller_has_action?(controller_class, :index) do
    before(:each) do
    # Test data is lazily created. Here we must force it to be created.
    records
    end

    it "requires authentication" do
    logout_example
    get resource_path
    expect(response).to require_login_api
    end

    it "enforces authorization" do
    expect_any_instance_of(Api::V1::BaseApiController).to
    receive(:policy_scope).and_call_original
    get resource_path
    end

    it "returns a 'OK' (200) HTTP status code" do
    get resource_path
    expect(response).to have_http_status(200)
    end

    it "returns all #{resource_plural}" do
    get resource_path
    # When testing the User model, a user created by the Devise login helper
    # increases the expected record count to 3.
    expected_count = resource_singular == :user ? 3 : 2
    expect(response_json.size).to eq(expected_count)
    end
    end


    describe "GET #{resource_path}/:id (#show)",
    if: controller_has_action?(controller_class, :show) do
    it "requires authentication" do
    logout_example
    get "#{resource_path}/#{record.id}"
    expect(response).to require_login_api
    end

    it "enforces authorization" do
    mock_authorization(authorized: false)
    get "#{resource_path}/#{record.id}"
    expect(response).to have_http_status(403)
    end

    context "with a valid #{resource_singular} ID" do
    before(:each) do
    get "#{resource_path}/#{record.id}"
    end

    it "returns a 'OK' (200) HTTP status code" do
    expect(response).to have_http_status(200)
    end

    it "returns the requested #{resource_singular}" do
    expect(response_json).to include(
    record.attributes.slice(comparable_attributes))
    end
    end

    context "with an invalid #{resource_singular} ID" do
    before(:each) { get "#{resource_path}/9999" }

    it "returns a 'not found' (404) status code" do
    expect(response).to have_http_status(404)
    end
    end
    end


    describe "POST #{resource_path} (#create)",
    if: controller_has_action?(controller_class, :create) do
    it "requires authentication" do
    logout_example
    post resource_path, { resource_singular => valid_attributes }
    expect(response).to require_login_api
    end

    it "enforces authorization" do
    mock_authorization(authorized: false)
    post resource_path, { resource_singular => valid_attributes }
    expect(response).to have_http_status(403)
    end

    context "with valid attributes" do
    before(:each) do
    post resource_path, { resource_singular => valid_attributes }
    end

    it "returns a 'created' (201) HTTP status code" do
    expect(response).to have_http_status(201)
    end

    it "returns the created #{resource_singular}" do
    expect(response_json).to include(
    record.attributes.slice(comparable_attributes))
    end
    end

    context "with invalid attributes" do
    before(:each) do
    post resource_path, { resource_singular => invalid_attributes }
    end

    it "returns a 'unprocessable entity' (422) HTTP status code" do
    expect(response).to have_http_status(422)
    end
    end
    end


    describe "PATCH #{resource_path}/:id (#update)",
    if: controller_has_action?(controller_class, :update) do
    it "requires authentication" do
    logout_example
    patch "#{resource_path}/#{record.id}",
    { resource_singular => valid_attributes }
    expect(response).to require_login_api
    end

    it "enforces authorization" do
    mock_authorization(authorized: false)
    patch "#{resource_path}/#{record.id}",
    { resource_singular => valid_attributes }
    expect(response).to have_http_status(403)
    end

    context "with valid attributes" do
    before(:each) do
    patch "#{resource_path}/#{record.id}",
    { resource_singular => valid_attributes }
    end

    it "returns a 'OK' (200) HTTP status code" do
    expect(response).to have_http_status(200)
    end

    it "returns the updated #{resource_singular}" do
    record.reload
    expect(response_json).to include(
    valid_attributes.slice(comparable_attributes))
    end
    end

    context "with invalid attributes" do
    before(:each) do
    patch "#{resource_path}/#{record.id}",
    { resource_singular => invalid_attributes }
    end

    it "returns an 'unprocessable entity' (422) status code" do
    expect(response).to have_http_status(422)
    end
    end
    end


    describe "DELETE #{resource_path}/:id (#destroy)",
    if: controller_has_action?(controller_class, :destroy) do
    it "requires authentication" do
    logout_example
    delete "#{resource_path}/#{record.id}"
    expect(response).to require_login_api
    end

    it "enforces authorization" do
    mock_authorization(authorized: false)
    delete "#{resource_path}/#{record.id}"
    expect(response).to have_http_status(403)
    end

    it "ensures the #{resource_singular} no longer exists" do
    delete "#{resource_path}/#{record.id}"
    # When testing the "user" resource, Devise unexpectedly logs out
    # (resulting in 401 to any further requests) after *any* user is deleted.
    login_admin if resource_singular == :user
    get "#{resource_path}/#{record.id}"
    expect(response).to have_http_status(404)
    end

    it "returns a 'no content' (204) status code" do
    delete "#{resource_path}/#{record.id}"
    expect(response).to have_http_status(204)
    end
    end
    end


    ... and this is a sample request spec:



    # spec/requests/users_spec.rb
    require "rails_helper"

    RSpec.describe "Users API", :type => :request do
    it_behaves_like "a RESTful JSON API",
    controller_class: Api::V1::UsersController,
    resource_path: "/api/v1/users",
    comparable_attributes: [:id, :email, :first_name, :last_name]
    end


    Thanks in advance.










    share|improve this question
















    bumped to the homepage by Community 10 hours ago


    This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.

















      up vote
      2
      down vote

      favorite
      1









      up vote
      2
      down vote

      favorite
      1






      1





      I have written the following shared examples which are used in multiple request specs to test a namespaced RESTful JSON API.



      Application details: Rails 4.2, RSpec 3.5, Devise for authentication and Pundit for authorization. Authorization policies are tested separarely as discussed in this post.



      I would greatly appreciate your feedback and suggestions on how to improve the specs (e.g., efficiency, readability, maintainability, DRYness).



      Here are the shared examples:



      RSpec.shared_examples "a RESTful JSON API",
      http_error_instead_of_exception: true do |controller_class:,
      resource_path:,
      comparable_attributes:|

      def self.controller_has_action?(controller_class, action)
      controller_class.action_methods.include?(action.to_s)
      end

      # Ensure authorization (Pundit gem) is enforced
      def mock_authorization(authorized: false)
      # Avoid Pundit::AuthorizationNotPerformedError when using "after_action
      # :verify_authorized". Use "allow" and not "expect" as #verify_authorized is
      # only called when we do not raise Pundit::NotAuthorizedError.
      allow_any_instance_of(Api::V1::BaseApiController).to
      receive(:verify_authorized)

      expectation = expect_any_instance_of(Api::V1::BaseApiController).to
      receive(:authorize)
      # Simulate a "not authorized" scenario
      expectation.and_raise(Pundit::NotAuthorizedError) if !authorized
      end

      resource_singular = resource_path.split("/").last.singularize.to_sym
      resource_plural = resource_path.split("/").last.to_sym

      before(:each) { login_admin }

      let(:record) { FactoryGirl.create(resource_singular) }
      let(:records) { FactoryGirl.create_pair(resource_singular) }
      # Models that validate the presence of associated records require some
      # hacking in the factory to include associations in #attributes_for
      let(:valid_attributes) { FactoryGirl.attributes_for(resource_singular) }
      # All factories must have a trait called :invalid
      let(:invalid_attributes) do
      FactoryGirl.attributes_for(resource_singular, :invalid)
      end
      let(:response_json) { JSON.parse(response.body) }

      describe "GET #{resource_path} (#index)",
      if: controller_has_action?(controller_class, :index) do
      before(:each) do
      # Test data is lazily created. Here we must force it to be created.
      records
      end

      it "requires authentication" do
      logout_example
      get resource_path
      expect(response).to require_login_api
      end

      it "enforces authorization" do
      expect_any_instance_of(Api::V1::BaseApiController).to
      receive(:policy_scope).and_call_original
      get resource_path
      end

      it "returns a 'OK' (200) HTTP status code" do
      get resource_path
      expect(response).to have_http_status(200)
      end

      it "returns all #{resource_plural}" do
      get resource_path
      # When testing the User model, a user created by the Devise login helper
      # increases the expected record count to 3.
      expected_count = resource_singular == :user ? 3 : 2
      expect(response_json.size).to eq(expected_count)
      end
      end


      describe "GET #{resource_path}/:id (#show)",
      if: controller_has_action?(controller_class, :show) do
      it "requires authentication" do
      logout_example
      get "#{resource_path}/#{record.id}"
      expect(response).to require_login_api
      end

      it "enforces authorization" do
      mock_authorization(authorized: false)
      get "#{resource_path}/#{record.id}"
      expect(response).to have_http_status(403)
      end

      context "with a valid #{resource_singular} ID" do
      before(:each) do
      get "#{resource_path}/#{record.id}"
      end

      it "returns a 'OK' (200) HTTP status code" do
      expect(response).to have_http_status(200)
      end

      it "returns the requested #{resource_singular}" do
      expect(response_json).to include(
      record.attributes.slice(comparable_attributes))
      end
      end

      context "with an invalid #{resource_singular} ID" do
      before(:each) { get "#{resource_path}/9999" }

      it "returns a 'not found' (404) status code" do
      expect(response).to have_http_status(404)
      end
      end
      end


      describe "POST #{resource_path} (#create)",
      if: controller_has_action?(controller_class, :create) do
      it "requires authentication" do
      logout_example
      post resource_path, { resource_singular => valid_attributes }
      expect(response).to require_login_api
      end

      it "enforces authorization" do
      mock_authorization(authorized: false)
      post resource_path, { resource_singular => valid_attributes }
      expect(response).to have_http_status(403)
      end

      context "with valid attributes" do
      before(:each) do
      post resource_path, { resource_singular => valid_attributes }
      end

      it "returns a 'created' (201) HTTP status code" do
      expect(response).to have_http_status(201)
      end

      it "returns the created #{resource_singular}" do
      expect(response_json).to include(
      record.attributes.slice(comparable_attributes))
      end
      end

      context "with invalid attributes" do
      before(:each) do
      post resource_path, { resource_singular => invalid_attributes }
      end

      it "returns a 'unprocessable entity' (422) HTTP status code" do
      expect(response).to have_http_status(422)
      end
      end
      end


      describe "PATCH #{resource_path}/:id (#update)",
      if: controller_has_action?(controller_class, :update) do
      it "requires authentication" do
      logout_example
      patch "#{resource_path}/#{record.id}",
      { resource_singular => valid_attributes }
      expect(response).to require_login_api
      end

      it "enforces authorization" do
      mock_authorization(authorized: false)
      patch "#{resource_path}/#{record.id}",
      { resource_singular => valid_attributes }
      expect(response).to have_http_status(403)
      end

      context "with valid attributes" do
      before(:each) do
      patch "#{resource_path}/#{record.id}",
      { resource_singular => valid_attributes }
      end

      it "returns a 'OK' (200) HTTP status code" do
      expect(response).to have_http_status(200)
      end

      it "returns the updated #{resource_singular}" do
      record.reload
      expect(response_json).to include(
      valid_attributes.slice(comparable_attributes))
      end
      end

      context "with invalid attributes" do
      before(:each) do
      patch "#{resource_path}/#{record.id}",
      { resource_singular => invalid_attributes }
      end

      it "returns an 'unprocessable entity' (422) status code" do
      expect(response).to have_http_status(422)
      end
      end
      end


      describe "DELETE #{resource_path}/:id (#destroy)",
      if: controller_has_action?(controller_class, :destroy) do
      it "requires authentication" do
      logout_example
      delete "#{resource_path}/#{record.id}"
      expect(response).to require_login_api
      end

      it "enforces authorization" do
      mock_authorization(authorized: false)
      delete "#{resource_path}/#{record.id}"
      expect(response).to have_http_status(403)
      end

      it "ensures the #{resource_singular} no longer exists" do
      delete "#{resource_path}/#{record.id}"
      # When testing the "user" resource, Devise unexpectedly logs out
      # (resulting in 401 to any further requests) after *any* user is deleted.
      login_admin if resource_singular == :user
      get "#{resource_path}/#{record.id}"
      expect(response).to have_http_status(404)
      end

      it "returns a 'no content' (204) status code" do
      delete "#{resource_path}/#{record.id}"
      expect(response).to have_http_status(204)
      end
      end
      end


      ... and this is a sample request spec:



      # spec/requests/users_spec.rb
      require "rails_helper"

      RSpec.describe "Users API", :type => :request do
      it_behaves_like "a RESTful JSON API",
      controller_class: Api::V1::UsersController,
      resource_path: "/api/v1/users",
      comparable_attributes: [:id, :email, :first_name, :last_name]
      end


      Thanks in advance.










      share|improve this question















      I have written the following shared examples which are used in multiple request specs to test a namespaced RESTful JSON API.



      Application details: Rails 4.2, RSpec 3.5, Devise for authentication and Pundit for authorization. Authorization policies are tested separarely as discussed in this post.



      I would greatly appreciate your feedback and suggestions on how to improve the specs (e.g., efficiency, readability, maintainability, DRYness).



      Here are the shared examples:



      RSpec.shared_examples "a RESTful JSON API",
      http_error_instead_of_exception: true do |controller_class:,
      resource_path:,
      comparable_attributes:|

      def self.controller_has_action?(controller_class, action)
      controller_class.action_methods.include?(action.to_s)
      end

      # Ensure authorization (Pundit gem) is enforced
      def mock_authorization(authorized: false)
      # Avoid Pundit::AuthorizationNotPerformedError when using "after_action
      # :verify_authorized". Use "allow" and not "expect" as #verify_authorized is
      # only called when we do not raise Pundit::NotAuthorizedError.
      allow_any_instance_of(Api::V1::BaseApiController).to
      receive(:verify_authorized)

      expectation = expect_any_instance_of(Api::V1::BaseApiController).to
      receive(:authorize)
      # Simulate a "not authorized" scenario
      expectation.and_raise(Pundit::NotAuthorizedError) if !authorized
      end

      resource_singular = resource_path.split("/").last.singularize.to_sym
      resource_plural = resource_path.split("/").last.to_sym

      before(:each) { login_admin }

      let(:record) { FactoryGirl.create(resource_singular) }
      let(:records) { FactoryGirl.create_pair(resource_singular) }
      # Models that validate the presence of associated records require some
      # hacking in the factory to include associations in #attributes_for
      let(:valid_attributes) { FactoryGirl.attributes_for(resource_singular) }
      # All factories must have a trait called :invalid
      let(:invalid_attributes) do
      FactoryGirl.attributes_for(resource_singular, :invalid)
      end
      let(:response_json) { JSON.parse(response.body) }

      describe "GET #{resource_path} (#index)",
      if: controller_has_action?(controller_class, :index) do
      before(:each) do
      # Test data is lazily created. Here we must force it to be created.
      records
      end

      it "requires authentication" do
      logout_example
      get resource_path
      expect(response).to require_login_api
      end

      it "enforces authorization" do
      expect_any_instance_of(Api::V1::BaseApiController).to
      receive(:policy_scope).and_call_original
      get resource_path
      end

      it "returns a 'OK' (200) HTTP status code" do
      get resource_path
      expect(response).to have_http_status(200)
      end

      it "returns all #{resource_plural}" do
      get resource_path
      # When testing the User model, a user created by the Devise login helper
      # increases the expected record count to 3.
      expected_count = resource_singular == :user ? 3 : 2
      expect(response_json.size).to eq(expected_count)
      end
      end


      describe "GET #{resource_path}/:id (#show)",
      if: controller_has_action?(controller_class, :show) do
      it "requires authentication" do
      logout_example
      get "#{resource_path}/#{record.id}"
      expect(response).to require_login_api
      end

      it "enforces authorization" do
      mock_authorization(authorized: false)
      get "#{resource_path}/#{record.id}"
      expect(response).to have_http_status(403)
      end

      context "with a valid #{resource_singular} ID" do
      before(:each) do
      get "#{resource_path}/#{record.id}"
      end

      it "returns a 'OK' (200) HTTP status code" do
      expect(response).to have_http_status(200)
      end

      it "returns the requested #{resource_singular}" do
      expect(response_json).to include(
      record.attributes.slice(comparable_attributes))
      end
      end

      context "with an invalid #{resource_singular} ID" do
      before(:each) { get "#{resource_path}/9999" }

      it "returns a 'not found' (404) status code" do
      expect(response).to have_http_status(404)
      end
      end
      end


      describe "POST #{resource_path} (#create)",
      if: controller_has_action?(controller_class, :create) do
      it "requires authentication" do
      logout_example
      post resource_path, { resource_singular => valid_attributes }
      expect(response).to require_login_api
      end

      it "enforces authorization" do
      mock_authorization(authorized: false)
      post resource_path, { resource_singular => valid_attributes }
      expect(response).to have_http_status(403)
      end

      context "with valid attributes" do
      before(:each) do
      post resource_path, { resource_singular => valid_attributes }
      end

      it "returns a 'created' (201) HTTP status code" do
      expect(response).to have_http_status(201)
      end

      it "returns the created #{resource_singular}" do
      expect(response_json).to include(
      record.attributes.slice(comparable_attributes))
      end
      end

      context "with invalid attributes" do
      before(:each) do
      post resource_path, { resource_singular => invalid_attributes }
      end

      it "returns a 'unprocessable entity' (422) HTTP status code" do
      expect(response).to have_http_status(422)
      end
      end
      end


      describe "PATCH #{resource_path}/:id (#update)",
      if: controller_has_action?(controller_class, :update) do
      it "requires authentication" do
      logout_example
      patch "#{resource_path}/#{record.id}",
      { resource_singular => valid_attributes }
      expect(response).to require_login_api
      end

      it "enforces authorization" do
      mock_authorization(authorized: false)
      patch "#{resource_path}/#{record.id}",
      { resource_singular => valid_attributes }
      expect(response).to have_http_status(403)
      end

      context "with valid attributes" do
      before(:each) do
      patch "#{resource_path}/#{record.id}",
      { resource_singular => valid_attributes }
      end

      it "returns a 'OK' (200) HTTP status code" do
      expect(response).to have_http_status(200)
      end

      it "returns the updated #{resource_singular}" do
      record.reload
      expect(response_json).to include(
      valid_attributes.slice(comparable_attributes))
      end
      end

      context "with invalid attributes" do
      before(:each) do
      patch "#{resource_path}/#{record.id}",
      { resource_singular => invalid_attributes }
      end

      it "returns an 'unprocessable entity' (422) status code" do
      expect(response).to have_http_status(422)
      end
      end
      end


      describe "DELETE #{resource_path}/:id (#destroy)",
      if: controller_has_action?(controller_class, :destroy) do
      it "requires authentication" do
      logout_example
      delete "#{resource_path}/#{record.id}"
      expect(response).to require_login_api
      end

      it "enforces authorization" do
      mock_authorization(authorized: false)
      delete "#{resource_path}/#{record.id}"
      expect(response).to have_http_status(403)
      end

      it "ensures the #{resource_singular} no longer exists" do
      delete "#{resource_path}/#{record.id}"
      # When testing the "user" resource, Devise unexpectedly logs out
      # (resulting in 401 to any further requests) after *any* user is deleted.
      login_admin if resource_singular == :user
      get "#{resource_path}/#{record.id}"
      expect(response).to have_http_status(404)
      end

      it "returns a 'no content' (204) status code" do
      delete "#{resource_path}/#{record.id}"
      expect(response).to have_http_status(204)
      end
      end
      end


      ... and this is a sample request spec:



      # spec/requests/users_spec.rb
      require "rails_helper"

      RSpec.describe "Users API", :type => :request do
      it_behaves_like "a RESTful JSON API",
      controller_class: Api::V1::UsersController,
      resource_path: "/api/v1/users",
      comparable_attributes: [:id, :email, :first_name, :last_name]
      end


      Thanks in advance.







      ruby unit-testing ruby-on-rails api rspec






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited Jul 13 '17 at 21:45

























      asked Jul 10 '17 at 21:25









      BrunoFacca

      658




      658





      bumped to the homepage by Community 10 hours ago


      This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.







      bumped to the homepage by Community 10 hours ago


      This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.
























          1 Answer
          1






          active

          oldest

          votes

















          up vote
          0
          down vote













          A suggestion would like to give, not sure how useful it would turn out for you.





          1. Separate out the controller level validations from Model level validations.


          For validating the data for the users whether they have mentioned attributes or not, "comparable_attributes: [:id, :email, :first_name, :last_name]", mention such validation at Model level spec file.



          For instance:



          require 'rails_helper'

          describe User do
          #Tests for fields
          it {is_expected.to respond_to :firstname}
          it {is_expected.to respond_to :lastname}
          it {is_expected.to respond_to :email}

          #Tests for validations
          describe 'validate lengths' do
          it {is_expected.to validate_length_of(:email).is_at_most(255)}
          it {is_expected.to validate_length_of(:firstname).is_at_most(255)}
          it {is_expected.to validate_length_of(:lastname).is_at_most(255)}
          end



          describe 'validate presence' do
          it {is_expected.to validate_presence_of :firstname }
          it {is_expected.to validate_presence_of :lastname }
          end

          end


          Create a user in the model with valid parameters and set it in the current_user and that same object can be used in the controller, where you can authenticate various controller request






          share|improve this answer





















          • Thanks for your reply. Besides request specs, the application also has model specs which test validations and other model behaviors. The comparable_attributes in the API tests serve to ensure that records which are retrieved, created or updated via API have the expected attribute values. Hence, it is not the same as testing the model. Regarding the creation of test data (e.g., a valid user object), I'm using Factory Girl.
            – BrunoFacca
            Jul 19 '17 at 12:26











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f168903%2frails-rspec-testing-api-with-request-specs-and-shared-examples%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          1 Answer
          1






          active

          oldest

          votes








          1 Answer
          1






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes








          up vote
          0
          down vote













          A suggestion would like to give, not sure how useful it would turn out for you.





          1. Separate out the controller level validations from Model level validations.


          For validating the data for the users whether they have mentioned attributes or not, "comparable_attributes: [:id, :email, :first_name, :last_name]", mention such validation at Model level spec file.



          For instance:



          require 'rails_helper'

          describe User do
          #Tests for fields
          it {is_expected.to respond_to :firstname}
          it {is_expected.to respond_to :lastname}
          it {is_expected.to respond_to :email}

          #Tests for validations
          describe 'validate lengths' do
          it {is_expected.to validate_length_of(:email).is_at_most(255)}
          it {is_expected.to validate_length_of(:firstname).is_at_most(255)}
          it {is_expected.to validate_length_of(:lastname).is_at_most(255)}
          end



          describe 'validate presence' do
          it {is_expected.to validate_presence_of :firstname }
          it {is_expected.to validate_presence_of :lastname }
          end

          end


          Create a user in the model with valid parameters and set it in the current_user and that same object can be used in the controller, where you can authenticate various controller request






          share|improve this answer





















          • Thanks for your reply. Besides request specs, the application also has model specs which test validations and other model behaviors. The comparable_attributes in the API tests serve to ensure that records which are retrieved, created or updated via API have the expected attribute values. Hence, it is not the same as testing the model. Regarding the creation of test data (e.g., a valid user object), I'm using Factory Girl.
            – BrunoFacca
            Jul 19 '17 at 12:26















          up vote
          0
          down vote













          A suggestion would like to give, not sure how useful it would turn out for you.





          1. Separate out the controller level validations from Model level validations.


          For validating the data for the users whether they have mentioned attributes or not, "comparable_attributes: [:id, :email, :first_name, :last_name]", mention such validation at Model level spec file.



          For instance:



          require 'rails_helper'

          describe User do
          #Tests for fields
          it {is_expected.to respond_to :firstname}
          it {is_expected.to respond_to :lastname}
          it {is_expected.to respond_to :email}

          #Tests for validations
          describe 'validate lengths' do
          it {is_expected.to validate_length_of(:email).is_at_most(255)}
          it {is_expected.to validate_length_of(:firstname).is_at_most(255)}
          it {is_expected.to validate_length_of(:lastname).is_at_most(255)}
          end



          describe 'validate presence' do
          it {is_expected.to validate_presence_of :firstname }
          it {is_expected.to validate_presence_of :lastname }
          end

          end


          Create a user in the model with valid parameters and set it in the current_user and that same object can be used in the controller, where you can authenticate various controller request






          share|improve this answer





















          • Thanks for your reply. Besides request specs, the application also has model specs which test validations and other model behaviors. The comparable_attributes in the API tests serve to ensure that records which are retrieved, created or updated via API have the expected attribute values. Hence, it is not the same as testing the model. Regarding the creation of test data (e.g., a valid user object), I'm using Factory Girl.
            – BrunoFacca
            Jul 19 '17 at 12:26













          up vote
          0
          down vote










          up vote
          0
          down vote









          A suggestion would like to give, not sure how useful it would turn out for you.





          1. Separate out the controller level validations from Model level validations.


          For validating the data for the users whether they have mentioned attributes or not, "comparable_attributes: [:id, :email, :first_name, :last_name]", mention such validation at Model level spec file.



          For instance:



          require 'rails_helper'

          describe User do
          #Tests for fields
          it {is_expected.to respond_to :firstname}
          it {is_expected.to respond_to :lastname}
          it {is_expected.to respond_to :email}

          #Tests for validations
          describe 'validate lengths' do
          it {is_expected.to validate_length_of(:email).is_at_most(255)}
          it {is_expected.to validate_length_of(:firstname).is_at_most(255)}
          it {is_expected.to validate_length_of(:lastname).is_at_most(255)}
          end



          describe 'validate presence' do
          it {is_expected.to validate_presence_of :firstname }
          it {is_expected.to validate_presence_of :lastname }
          end

          end


          Create a user in the model with valid parameters and set it in the current_user and that same object can be used in the controller, where you can authenticate various controller request






          share|improve this answer












          A suggestion would like to give, not sure how useful it would turn out for you.





          1. Separate out the controller level validations from Model level validations.


          For validating the data for the users whether they have mentioned attributes or not, "comparable_attributes: [:id, :email, :first_name, :last_name]", mention such validation at Model level spec file.



          For instance:



          require 'rails_helper'

          describe User do
          #Tests for fields
          it {is_expected.to respond_to :firstname}
          it {is_expected.to respond_to :lastname}
          it {is_expected.to respond_to :email}

          #Tests for validations
          describe 'validate lengths' do
          it {is_expected.to validate_length_of(:email).is_at_most(255)}
          it {is_expected.to validate_length_of(:firstname).is_at_most(255)}
          it {is_expected.to validate_length_of(:lastname).is_at_most(255)}
          end



          describe 'validate presence' do
          it {is_expected.to validate_presence_of :firstname }
          it {is_expected.to validate_presence_of :lastname }
          end

          end


          Create a user in the model with valid parameters and set it in the current_user and that same object can be used in the controller, where you can authenticate various controller request







          share|improve this answer












          share|improve this answer



          share|improve this answer










          answered Jul 19 '17 at 3:39









          Neha Chopra

          1313




          1313












          • Thanks for your reply. Besides request specs, the application also has model specs which test validations and other model behaviors. The comparable_attributes in the API tests serve to ensure that records which are retrieved, created or updated via API have the expected attribute values. Hence, it is not the same as testing the model. Regarding the creation of test data (e.g., a valid user object), I'm using Factory Girl.
            – BrunoFacca
            Jul 19 '17 at 12:26


















          • Thanks for your reply. Besides request specs, the application also has model specs which test validations and other model behaviors. The comparable_attributes in the API tests serve to ensure that records which are retrieved, created or updated via API have the expected attribute values. Hence, it is not the same as testing the model. Regarding the creation of test data (e.g., a valid user object), I'm using Factory Girl.
            – BrunoFacca
            Jul 19 '17 at 12:26
















          Thanks for your reply. Besides request specs, the application also has model specs which test validations and other model behaviors. The comparable_attributes in the API tests serve to ensure that records which are retrieved, created or updated via API have the expected attribute values. Hence, it is not the same as testing the model. Regarding the creation of test data (e.g., a valid user object), I'm using Factory Girl.
          – BrunoFacca
          Jul 19 '17 at 12:26




          Thanks for your reply. Besides request specs, the application also has model specs which test validations and other model behaviors. The comparable_attributes in the API tests serve to ensure that records which are retrieved, created or updated via API have the expected attribute values. Hence, it is not the same as testing the model. Regarding the creation of test data (e.g., a valid user object), I'm using Factory Girl.
          – BrunoFacca
          Jul 19 '17 at 12:26


















          draft saved

          draft discarded




















































          Thanks for contributing an answer to Code Review Stack Exchange!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          Use MathJax to format equations. MathJax reference.


          To learn more, see our tips on writing great answers.





          Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


          Please pay close attention to the following guidance:


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f168903%2frails-rspec-testing-api-with-request-specs-and-shared-examples%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          List directoties down one level, excluding some named directories and files

          list processes belonging to a network namespace

          list systemd RuntimeDirectory mounts