Back to Repositories

Testing Error Handler Isolation in Mounted Grape APIs

This test suite validates the error handling behavior in Grape API when multiple classes define rescue_from handlers. It specifically examines how different mounted API classes handle ZeroDivisionError exceptions and verifies the isolation of error handling between mounted endpoints.

Test Coverage Overview

The test suite covers error handling scenarios across multiple mounted Grape API classes.

Key areas tested include:
  • Independent rescue_from handlers in different API classes
  • Correct error status code and response body verification
  • Error handler isolation between mounted endpoints
  • Behavior when a mounted class lacks error handling

Implementation Analysis

The testing approach uses RSpec to create dynamic Grape API classes with different error handling configurations. It implements multiple test scenarios using nested contexts and let blocks to establish various API configurations.

The tests utilize Grape’s mounting capabilities and custom error handlers, demonstrating how rescue_from directives work in isolation across different API classes.

Technical Details

Testing tools and components:
  • RSpec for test framework
  • Grape API framework
  • Dynamic class creation using Class.new
  • Custom error handlers with error! method
  • HTTP status code validation
  • JSON response body verification

Best Practices Demonstrated

The test suite exemplifies several testing best practices in API development.

Notable practices include:
  • Isolation of test scenarios using contexts
  • Clear separation of concerns in error handling
  • Comprehensive edge case coverage
  • Dynamic test setup using let blocks
  • Explicit expectation setting for both success and failure cases

ruby-grape/grape

spec/grape/api/mount_and_rescue_from_spec.rb

            
# frozen_string_literal: true

describe Grape::API do
  context 'when multiple classes defines the same rescue_from' do
    let(:an_api) do
      Class.new(Grape::API) do
        rescue_from ZeroDivisionError do
          error!({ type: 'an-api-zero' }, 404)
        end

        get '/an-api' do
          { count: 1 / 0 }
        end
      end
    end
    let(:another_api) do
      Class.new(Grape::API) do
        rescue_from ZeroDivisionError do
          error!({ type: 'another-api-zero' }, 322)
        end

        get '/another-api' do
          { count: 1 / 0 }
        end
      end
    end
    let(:other_main) do
      context = self

      Class.new(Grape::API) do
        mount context.an_api
        mount context.another_api
      end
    end

    def app
      other_main
    end

    it 'is rescued by the rescue_from ZeroDivisionError handler defined inside each of the classes' do
      get '/an-api'

      expect(last_response.status).to eq(404)
      expect(last_response.body).to eq({ type: 'an-api-zero' }.to_json)

      get '/another-api'

      expect(last_response.status).to eq(322)
      expect(last_response.body).to eq({ type: 'another-api-zero' }.to_json)
    end

    context 'when some class does not define a rescue_from but it was defined in a previous mounted endpoint' do
      let(:an_api_without_defined_rescue_from) do
        Class.new(Grape::API) do
          get '/another-api-without-defined-rescue-from' do
            { count: 1 / 0 }
          end
        end
      end
      let(:other_main_with_not_defined_rescue_from) do
        context = self

        Class.new(Grape::API) do
          mount context.an_api
          mount context.another_api
          mount context.an_api_without_defined_rescue_from
        end
      end

      def app
        other_main_with_not_defined_rescue_from
      end

      it 'is not rescued by any of the previous defined rescue_from ZeroDivisionError handlers' do
        get '/an-api'

        expect(last_response.status).to eq(404)
        expect(last_response.body).to eq({ type: 'an-api-zero' }.to_json)

        get '/another-api'

        expect(last_response.status).to eq(322)
        expect(last_response.body).to eq({ type: 'another-api-zero' }.to_json)

        expect do
          get '/another-api-without-defined-rescue-from'
        end.to raise_error(ZeroDivisionError)
      end
    end
  end
end