Back to Repositories

Testing Middleware Component Implementation in Faraday

This test suite examines the Faraday::Middleware functionality, focusing on middleware initialization, request handling, and configuration options. It validates core middleware behaviors including request/error handling and default options management in the Faraday HTTP client library.

Test Coverage Overview

The test suite provides comprehensive coverage of Faraday::Middleware functionality:
  • Middleware initialization with custom options
  • Request and error handling hooks
  • Resource cleanup through close method
  • Default options inheritance and configuration
  • Edge cases for options validation and error handling

Implementation Analysis

The testing approach uses RSpec’s behavior-driven development patterns with extensive use of doubles and expectation matching. The implementation leverages shared contexts for setup/teardown, subject/let blocks for test isolation, and nested describe/context blocks for organized test grouping.

Key patterns include mocking HTTP responses, validating middleware hooks, and testing inheritance chains.

Technical Details

Testing tools and configuration:
  • RSpec as the testing framework
  • Test doubles for HTTP request simulation
  • Shared contexts for managing test state
  • Custom matcher implementations
  • Faraday test adapter for HTTP interaction testing

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Isolated test cases with clear setup and teardown
  • Comprehensive error case coverage
  • DRY principles through shared contexts
  • Clear test organization with descriptive contexts
  • Thorough validation of public interfaces

lostisland/faraday

spec/faraday/middleware_spec.rb

            
# frozen_string_literal: true

RSpec.describe Faraday::Middleware do
  subject { described_class.new(app) }
  let(:app) { double }

  describe 'options' do
    context 'when options are passed to the middleware' do
      subject { described_class.new(app, options) }
      let(:options) { { field: 'value' } }

      it 'accepts options when initialized' do
        expect(subject.options[:field]).to eq('value')
      end
    end
  end

  describe '#on_request' do
    subject do
      Class.new(described_class) do
        def on_request(env)
          # do nothing
        end
      end.new(app)
    end

    it 'is called by #call' do
      expect(app).to receive(:call).and_return(app)
      expect(app).to receive(:on_complete)
      is_expected.to receive(:call).and_call_original
      is_expected.to receive(:on_request)
      subject.call(double)
    end
  end

  describe '#on_error' do
    subject do
      Class.new(described_class) do
        def on_error(error)
          # do nothing
        end
      end.new(app)
    end

    it 'is called by #call' do
      expect(app).to receive(:call).and_raise(Faraday::ConnectionFailed)
      is_expected.to receive(:call).and_call_original
      is_expected.to receive(:on_error)

      expect { subject.call(double) }.to raise_error(Faraday::ConnectionFailed)
    end
  end

  describe '#close' do
    context "with app that doesn't support \#close" do
      it 'should issue warning' do
        is_expected.to receive(:warn)
        subject.close
      end
    end

    context "with app that supports \#close" do
      it 'should issue warning' do
        expect(app).to receive(:close)
        is_expected.to_not receive(:warn)
        subject.close
      end
    end
  end

  describe '::default_options' do
    let(:subclass_no_options) { FaradayMiddlewareSubclasses::SubclassNoOptions }
    let(:subclass_one_option) { FaradayMiddlewareSubclasses::SubclassOneOption }
    let(:subclass_two_options) { FaradayMiddlewareSubclasses::SubclassTwoOptions }

    def build_conn(resp_middleware)
      Faraday.new do |c|
        c.adapter :test do |stub|
          stub.get('/success') { [200, {}, 'ok'] }
        end
        c.response resp_middleware
      end
    end

    RSpec.shared_context 'reset @default_options' do
      before(:each) do
        FaradayMiddlewareSubclasses::SubclassNoOptions.instance_variable_set(:@default_options, nil)
        FaradayMiddlewareSubclasses::SubclassOneOption.instance_variable_set(:@default_options, nil)
        FaradayMiddlewareSubclasses::SubclassTwoOptions.instance_variable_set(:@default_options, nil)
        Faraday::Middleware.instance_variable_set(:@default_options, nil)
      end
    end

    after(:all) do
      FaradayMiddlewareSubclasses::SubclassNoOptions.instance_variable_set(:@default_options, nil)
      FaradayMiddlewareSubclasses::SubclassOneOption.instance_variable_set(:@default_options, nil)
      FaradayMiddlewareSubclasses::SubclassTwoOptions.instance_variable_set(:@default_options, nil)
      Faraday::Middleware.instance_variable_set(:@default_options, nil)
    end

    context 'with subclass DEFAULT_OPTIONS defined' do
      include_context 'reset @default_options'

      context 'and without application options configured' do
        let(:resp1) { build_conn(:one_option).get('/success') }

        it 'has only subclass defaults' do
          expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
          expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS)
          expect(subclass_one_option.default_options).to eq(subclass_one_option::DEFAULT_OPTIONS)
          expect(subclass_two_options.default_options).to eq(subclass_two_options::DEFAULT_OPTIONS)
        end

        it { expect(resp1.body).to eq('ok') }
      end

      context "and with one application's options changed" do
        let(:resp2) { build_conn(:two_options).get('/success') }

        before(:each) do
          FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false }
        end

        it 'only updates default options of target subclass' do
          expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
          expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS)
          expect(subclass_one_option.default_options).to eq(subclass_one_option::DEFAULT_OPTIONS)
          expect(subclass_two_options.default_options).to eq({ some_option: false, some_other_option: false })
        end

        it { expect(resp2.body).to eq('ok') }
      end

      context "and with two applications' options changed" do
        let(:resp1) { build_conn(:one_option).get('/success') }
        let(:resp2) { build_conn(:two_options).get('/success') }

        before(:each) do
          FaradayMiddlewareSubclasses::SubclassOneOption.default_options = { some_other_option: true }
          FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false }
        end

        it 'updates subclasses and parent independent of each other' do
          expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
          expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS)
          expect(subclass_one_option.default_options).to eq({ some_other_option: true })
          expect(subclass_two_options.default_options).to eq({ some_option: false, some_other_option: false })
        end

        it { expect(resp1.body).to eq('ok') }
        it { expect(resp2.body).to eq('ok') }
      end
    end

    context 'with FARADAY::MIDDLEWARE DEFAULT_OPTIONS and with Subclass DEFAULT_OPTIONS' do
      before(:each) do
        stub_const('Faraday::Middleware::DEFAULT_OPTIONS', { its_magic: false })
      end

      # Must stub Faraday::Middleware::DEFAULT_OPTIONS before resetting default options
      include_context 'reset @default_options'

      context 'and without application options configured' do
        let(:resp1) { build_conn(:one_option).get('/success') }

        it 'has only subclass defaults' do
          expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
          expect(FaradayMiddlewareSubclasses::SubclassNoOptions.default_options).to eq({ its_magic: false })
          expect(FaradayMiddlewareSubclasses::SubclassOneOption.default_options).to eq({ its_magic: false, some_other_option: false })
          expect(FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options).to eq({ its_magic: false, some_option: true, some_other_option: false })
        end

        it { expect(resp1.body).to eq('ok') }
      end

      context "and with two applications' options changed" do
        let(:resp1) { build_conn(:one_option).get('/success') }
        let(:resp2) { build_conn(:two_options).get('/success') }

        before(:each) do
          FaradayMiddlewareSubclasses::SubclassOneOption.default_options = { some_other_option: true }
          FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false }
        end

        it 'updates subclasses and parent independent of each other' do
          expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
          expect(FaradayMiddlewareSubclasses::SubclassNoOptions.default_options).to eq({ its_magic: false })
          expect(FaradayMiddlewareSubclasses::SubclassOneOption.default_options).to eq({ its_magic: false, some_other_option: true })
          expect(FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options).to eq({ its_magic: false, some_option: false, some_other_option: false })
        end

        it { expect(resp1.body).to eq('ok') }
        it { expect(resp2.body).to eq('ok') }
      end
    end

    describe 'default_options input validation' do
      include_context 'reset @default_options'

      it 'raises error if Faraday::Middleware option does not exist' do
        expect { Faraday::Middleware.default_options = { something_special: true } }.to raise_error(Faraday::InitializationError) do |e|
          expect(e.message).to eq('Invalid options provided. Keys not found in Faraday::Middleware::DEFAULT_OPTIONS: something_special')
        end
      end

      it 'raises error if subclass option does not exist' do
        expect { subclass_one_option.default_options = { this_is_a_typo: true } }.to raise_error(Faraday::InitializationError) do |e|
          expect(e.message).to eq('Invalid options provided. Keys not found in FaradayMiddlewareSubclasses::SubclassOneOption::DEFAULT_OPTIONS: this_is_a_typo')
        end
      end
    end
  end
end