Back to Repositories

Testing Grape DSL Routing Implementation in ruby-grape/grape

This test suite examines the routing capabilities of the Grape DSL (Domain Specific Language) framework. It verifies core routing functionalities including version management, prefix handling, endpoint creation, and namespace operations. The tests ensure proper API routing behavior and configuration management.

Test Coverage Overview

The test suite provides comprehensive coverage of Grape’s routing DSL components:
  • Version and prefix management for routes
  • Endpoint creation and route handling
  • Namespace and mounting capabilities
  • HTTP method implementations (GET, POST, PUT, etc.)
  • Route parameter validation and requirements

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development patterns with extensive mocking and stubbing. The tests employ dummy classes with Grape::DSL::Routing inclusion to isolate routing functionality. Implementation focuses on verifying method delegation, namespace inheritance, and route configuration persistence.

Technical Details

Testing infrastructure includes:
  • RSpec as the testing framework
  • Rack integration for HTTP method testing
  • Mock objects for isolation testing
  • Custom class implementations for namespace testing
  • Grape::API class extensions

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Isolated test contexts using let blocks
  • Comprehensive method stubbing
  • Clear test case organization
  • Thorough edge case coverage
  • Consistent testing patterns across different routing features

ruby-grape/grape

spec/grape/dsl/routing_spec.rb

            
# frozen_string_literal: true

describe Grape::DSL::Routing do
  subject { dummy_class }

  let(:dummy_class) do
    Class.new do
      include Grape::DSL::Routing
    end
  end

  let(:proc) { -> {} }
  let(:options) { { a: :b } }
  let(:path) { '/dummy' }

  describe '.version' do
    it 'sets a version for route' do
      version = 'v1'
      expect(subject).to receive(:namespace_inheritable).with(:version, [version])
      expect(subject).to receive(:namespace_inheritable).with(:version_options, { using: :path })
      expect(subject.version(version)).to eq(version)
    end
  end

  describe '.prefix' do
    it 'sets a prefix for route' do
      prefix = '/api'
      expect(subject).to receive(:namespace_inheritable).with(:root_prefix, prefix)
      subject.prefix prefix
    end
  end

  describe '.scope' do
    it 'create a scope without affecting the URL' do
      expect(subject).to receive(:within_namespace)
      subject.scope {}
    end
  end

  describe '.do_not_route_head!' do
    it 'sets do not route head option' do
      expect(subject).to receive(:namespace_inheritable).with(:do_not_route_head, true)
      subject.do_not_route_head!
    end
  end

  describe '.do_not_route_options!' do
    it 'sets do not route options option' do
      expect(subject).to receive(:namespace_inheritable).with(:do_not_route_options, true)
      subject.do_not_route_options!
    end
  end

  describe '.mount' do
    it 'mounts on a nested path' do
      subject = Class.new(Grape::API)
      app1 = Class.new(Grape::API)
      app2 = Class.new(Grape::API)
      app2.get '/nice' do
        'play'
      end

      subject.mount app1 => '/app1'
      app1.mount app2 => '/app2'

      expect(subject.inheritable_setting.to_hash[:namespace]).to eq({})
      expect(subject.inheritable_setting.to_hash[:namespace_inheritable]).to eq({})
      expect(app1.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1'])

      expect(app2.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1', '/app2'])
    end

    it 'mounts multiple routes at once' do
      base_app = Class.new(Grape::API)
      app1     = Class.new(Grape::API)
      app2     = Class.new(Grape::API)
      base_app.mount(app1 => '/app1', app2 => '/app2')

      expect(app1.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app1'])
      expect(app2.inheritable_setting.to_hash[:namespace_stackable]).to eq(mount_path: ['/app2'])
    end
  end

  describe '.route' do
    before do
      allow(subject).to receive(:endpoints).and_return([])
      allow(subject).to receive(:route_end)
      allow(subject).to receive(:reset_validations!)
    end

    it 'marks end of the route' do
      expect(subject).to receive(:route_end)
      subject.route(:any)
    end

    it 'resets validations' do
      expect(subject).to receive(:reset_validations!)
      subject.route(:any)
    end

    it 'defines a new endpoint' do
      expect { subject.route(:any) }
        .to change { subject.endpoints.count }.from(0).to(1)
    end

    it 'does not duplicate identical endpoints' do
      subject.route(:any)
      expect { subject.route(:any) }
        .not_to change(subject.endpoints, :count)
    end

    it 'generates correct endpoint options' do
      allow(subject).to receive(:route_setting).with(:description).and_return(fiz: 'baz')
      allow(subject).to receive(:namespace_stackable_with_hash).and_return(nuz: 'naz')

      expect(Grape::Endpoint).to receive(:new) do |_inheritable_setting, endpoint_options|
        expect(endpoint_options[:method]).to eq :get
        expect(endpoint_options[:path]).to eq '/foo'
        expect(endpoint_options[:for]).to eq subject
        expect(endpoint_options[:route_options]).to eq(foo: 'bar', fiz: 'baz', params: { nuz: 'naz' })
      end.and_yield

      subject.route(:get, '/foo', { foo: 'bar' }, &proc {})
    end
  end

  describe '.get' do
    it 'delegates to .route' do
      expect(subject).to receive(:route).with(Rack::GET, path, options)
      subject.get path, options, &proc
    end
  end

  describe '.post' do
    it 'delegates to .route' do
      expect(subject).to receive(:route).with(Rack::POST, path, options)
      subject.post path, options, &proc
    end
  end

  describe '.put' do
    it 'delegates to .route' do
      expect(subject).to receive(:route).with(Rack::PUT, path, options)
      subject.put path, options, &proc
    end
  end

  describe '.head' do
    it 'delegates to .route' do
      expect(subject).to receive(:route).with(Rack::HEAD, path, options)
      subject.head path, options, &proc
    end
  end

  describe '.delete' do
    it 'delegates to .route' do
      expect(subject).to receive(:route).with(Rack::DELETE, path, options)
      subject.delete path, options, &proc
    end
  end

  describe '.options' do
    it 'delegates to .route' do
      expect(subject).to receive(:route).with(Rack::OPTIONS, path, options)
      subject.options path, options, &proc
    end
  end

  describe '.patch' do
    it 'delegates to .route' do
      expect(subject).to receive(:route).with(Rack::PATCH, path, options)
      subject.patch path, options, &proc
    end
  end

  describe '.namespace' do
    let(:new_namespace) { Object.new }

    it 'creates a new namespace with given name and options' do
      expect(subject).to receive(:within_namespace).and_yield
      expect(subject).to receive(:nest).and_yield
      expect(Grape::Namespace).to receive(:new).with(:foo, { foo: 'bar' }).and_return(new_namespace)
      expect(subject).to receive(:namespace_stackable).with(:namespace, new_namespace)

      subject.namespace :foo, foo: 'bar', &proc {}
    end

    it 'calls #joined_space_path on Namespace' do
      result_of_namspace_stackable = Object.new
      allow(subject).to receive(:namespace_stackable).and_return(result_of_namspace_stackable)
      expect(Grape::Namespace).to receive(:joined_space_path).with(result_of_namspace_stackable)
      subject.namespace
    end
  end

  describe '.group' do
    it 'is alias to #namespace' do
      expect(subject.method(:group)).to eq subject.method(:namespace)
    end
  end

  describe '.resource' do
    it 'is alias to #namespace' do
      expect(subject.method(:resource)).to eq subject.method(:namespace)
    end
  end

  describe '.resources' do
    it 'is alias to #namespace' do
      expect(subject.method(:resources)).to eq subject.method(:namespace)
    end
  end

  describe '.segment' do
    it 'is alias to #namespace' do
      expect(subject.method(:segment)).to eq subject.method(:namespace)
    end
  end

  describe '.routes' do
    let(:routes) { Object.new }

    it 'returns value received from #prepare_routes' do
      expect(subject).to receive(:prepare_routes).and_return(routes)
      expect(subject.routes).to eq routes
    end

    context 'when #routes was already called once' do
      before do
        allow(subject).to receive(:prepare_routes).and_return(routes)
        subject.routes
      end

      it 'does not call prepare_routes again' do
        expect(subject).not_to receive(:prepare_routes)
        expect(subject.routes).to eq routes
      end
    end
  end

  describe '.route_param' do
    let!(:options) { { requirements: regex } }
    let(:regex) { /(.*)/ }

    it 'calls #namespace with given params' do
      expect(subject).to receive(:namespace).with(':foo', {}).and_yield
      subject.route_param('foo', {}, &proc {})
    end

    it 'nests requirements option under param name' do
      expect(subject).to receive(:namespace) do |_param, options|
        expect(options[:requirements][:foo]).to eq regex
      end
      subject.route_param('foo', options, &proc {})
    end

    it 'does not modify options parameter' do
      allow(subject).to receive(:namespace)
      expect { subject.route_param('foo', options, &proc {}) }
        .not_to(change { options })
    end
  end

  describe '.versions' do
    it 'returns last defined version' do
      subject.version 'v1'
      subject.version 'v2'
      expect(subject.version).to eq('v2')
    end
  end
end