Back to Repositories

Testing Grape Middleware Base Implementation in ruby-grape/grape

This test suite validates the core functionality of Grape’s Base Middleware implementation, focusing on request/response handling, callback execution, and header management. It ensures proper middleware behavior in the Grape framework through comprehensive unit testing.

Test Coverage Overview

The test suite provides extensive coverage of Grape’s Base Middleware functionality:

  • Request/response cycle validation
  • Callback execution (before/after hooks)
  • Error handling scenarios
  • Response context management
  • Header manipulation and overwriting

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development patterns with a focus on middleware functionality verification. The tests employ mock objects and context-specific scenarios to validate middleware behavior across different use cases.

Key patterns include:
  • Subject/let block usage for test setup
  • Shared context management
  • Mock response handling
  • Dynamic class generation for test cases

Technical Details

Testing infrastructure includes:

  • RSpec as the testing framework
  • Rack::Builder for middleware stack testing
  • Rack::Response for response handling
  • Mock objects for isolation testing
  • Custom middleware implementations for specific test scenarios

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test contexts for different scenarios
  • Comprehensive error case coverage
  • Clear test organization and naming
  • Effective use of before/after hooks
  • Proper separation of concerns in test cases

ruby-grape/grape

spec/grape/middleware/base_spec.rb

            
# frozen_string_literal: true

describe Grape::Middleware::Base do
  subject { described_class.new(blank_app) }

  let(:blank_app) { ->(_) { [200, {}, 'Hi there.'] } }

  before do
    # Keep it one object for testing.
    allow(subject).to receive(:dup).and_return(subject)
  end

  it 'has the app as an accessor' do
    expect(subject.app).to eq(blank_app)
  end

  it 'calls through to the app' do
    expect(subject.call({})).to eq([200, {}, 'Hi there.'])
  end

  context 'callbacks' do
    after { subject.call!({}) }

    it 'calls #before' do
      expect(subject).to receive(:before)
    end

    it 'calls #after' do
      expect(subject).to receive(:after)
    end
  end

  context 'callbacks on error' do
    let(:blank_app) { ->(_) { raise StandardError } }

    it 'calls #after' do
      expect(subject).to receive(:after)
      expect { subject.call({}) }.to raise_error(StandardError)
    end
  end

  context 'after callback' do
    before do
      allow(subject).to receive(:after).and_return([200, {}, 'Hello from after callback'])
    end

    it 'overwrites application response' do
      expect(subject.call!({}).last).to eq('Hello from after callback')
    end
  end

  context 'after callback with errors' do
    it 'does not overwrite the application response' do
      expect(subject.call({})).to eq([200, {}, 'Hi there.'])
    end

    context 'with patched warnings' do
      before do
        @warnings = warnings = []
        allow(subject).to receive(:warn) { |m| warnings << m }
        allow(subject).to receive(:after).and_raise(StandardError)
      end

      it 'does show a warning' do
        expect { subject.call({}) }.to raise_error(StandardError)
        expect(@warnings).not_to be_empty
      end
    end
  end

  it 'is able to access the response' do
    subject.call({})
    expect(subject.response).to be_a(Rack::Response)
  end

  describe '#response' do
    subject do
      described_class.new(response)
    end

    before { subject.call({}) }

    context 'when Array' do
      let(:rack_response) { Rack::Response.new('test', 204, abc: 1) }
      let(:response) { ->(_) { [204, { abc: 1 }, 'test'] } }

      it 'status' do
        expect(subject.response.status).to eq(204)
      end

      it 'body' do
        expect(subject.response.body).to eq(['test'])
      end

      it 'header' do
        expect(subject.response.headers).to have_key(:abc)
      end

      it 'returns the memoized Rack::Response instance' do
        allow(Rack::Response).to receive(:new).and_return(rack_response)
        expect(subject.response).to eq(rack_response)
      end
    end

    context 'when Rack::Response' do
      let(:rack_response) { Rack::Response.new('test', 204, abc: 1) }
      let(:response) { ->(_) { rack_response } }

      it 'status' do
        expect(subject.response.status).to eq(204)
      end

      it 'body' do
        expect(subject.response.body).to eq(['test'])
      end

      it 'header' do
        expect(subject.response.headers).to have_key(:abc)
      end

      it 'returns the memoized Rack::Response instance' do
        expect(subject.response).to eq(rack_response)
      end
    end
  end

  describe '#context' do
    subject { described_class.new(blank_app) }

    it 'allows access to response context' do
      subject.call(Grape::Env::API_ENDPOINT => { header: 'some header' })
      expect(subject.context).to eq(header: 'some header')
    end
  end

  context 'options' do
    it 'persists options passed at initialization' do
      expect(described_class.new(blank_app, abc: true).options[:abc]).to be true
    end

    context 'defaults' do
      let(:example_ware) do
        Class.new(Grape::Middleware::Base) do
          def default_options
            { monkey: true }
          end
        end
      end

      it 'persists the default options' do
        expect(example_ware.new(blank_app).options[:monkey]).to be true
      end

      it 'overrides default options when provided' do
        expect(example_ware.new(blank_app, monkey: false).options[:monkey]).to be false
      end
    end
  end

  context 'header' do
    let(:example_ware) do
      Class.new(Grape::Middleware::Base) do
        def before
          header 'X-Test-Before', 'Hi'
        end

        def after
          header 'X-Test-After', 'Bye'
          nil
        end
      end
    end

    let(:app) do
      context = self

      Rack::Builder.app do
        use context.example_ware
        run ->(_) { [200, {}, ['Yeah']] }
      end
    end

    it 'is able to set a header' do
      get '/'
      expect(last_response.headers['X-Test-Before']).to eq('Hi')
      expect(last_response.headers['X-Test-After']).to eq('Bye')
    end
  end

  context 'header overwrite' do
    let(:example_ware) do
      Class.new(Grape::Middleware::Base) do
        def before
          header 'X-Test-Overwriting', 'Hi'
        end

        def after
          header 'X-Test-Overwriting', 'Bye'
          nil
        end
      end
    end
    let(:api) do
      Class.new(Grape::API) do
        get('/') do
          header 'X-Test-Overwriting', 'Yeah'
          'Hello'
        end
      end
    end

    let(:app) do
      context = self

      Rack::Builder.app do
        use context.example_ware
        run context.api.new
      end
    end

    it 'overwrites header by after headers' do
      get '/'
      expect(last_response.headers['X-Test-Overwriting']).to eq('Bye')
    end
  end
end