元フリーエンジニアライフ

Ruby on Rails とか MovableType とかAWSやってるフリーランスウェブエンジニアの記録でした。現在は法人成りしてIT社長。

Rails + Grape + Rspec でサブドメイン(constraints)のテストを行う時は integration_session.host= を使う

問題

Rails.application.routes.draw do
  constraints subdomain: /^api/ do
    mount Api::HogeApi => '/hoge'
    mount Api::FugaApi => '/fuga'
  end
  constraints subdomain: /^(?!api)/ do
    # non API routes
  end

こんな感じのとき、Grapeのドキュメントにあるような

RSpec.configure do |config|
  config.include RSpec::Rails::RequestExampleGroup, type: :request, file_path: /spec\/api/
end

だけでは、

     Failure/Error: get "/api/hoge/entries/1"
     ActionController::RoutingError:
       No route matches [GET] "/api/hoge/entries/1"

のようなエラーになる。

対策

コードを追って、最終的にこんな感じでGrapeテストの時のホスト名を設定するようにした。

  # API
  config.include RSpec::Rails::RequestExampleGroup, type: :request, file_path: /spec\/api/
  config.include RSpec::Rails::ViewRendering, type: :request, file_path: /spec\/api/
  config.before do
    if self.class.described_class < Grape::API
      reset! unless integration_session
      integration_session.host = 'api.example.com'
    end
  end

※テスト中でreset!して複数リクエストのテストを行う場合、毎回integration_session.host =で設定する必要がある点に注意。

default_url_optionsを設定する方法ではうまくいかない。(リクエストの過程で参照されない)

過程

Rspec::Rails::RequestExampleGroup

      include ActionDispatch::Integration::Runner

ActionDispatch::Integration::Runner

    module Runner
      include ActionDispatch::Assertions

      def app
        @app ||= nil
      end

      # Reset the current session. This is useful for testing multiple sessions
      # in a single test case.
      def reset!
        @integration_session = Integration::Session.new(app)
      end

      def remove! # :nodoc:
        @integration_session = nil
      end

      %w(get post patch put head delete cookies assigns
         xml_http_request xhr get_via_redirect post_via_redirect).each do |method|
        define_method(method) do |*args|
          reset! unless integration_session

          # reset the html_document variable, except for cookies/assigns calls
          unless method == 'cookies' || method == 'assigns'
            @html_document = nil
            reset_template_assertion
          end

          integration_session.__send__(method, *args).tap do
            copy_session_variables!
          end
        end
      end

Integration::Session#get

      def get(path, parameters = nil, headers_or_env = nil)
        process :get, path, parameters, headers_or_env
      end

Integration::Session#process

        # Performs the actual request.
        def process(method, path, parameters = nil, headers_or_env = nil)
          if path =~ %r{://}
            location = URI.parse(path)
            https! URI::HTTPS === location if location.scheme
            host! "#{location.host}:#{location.port}" if location.host
            path = location.query ? "#{location.path}?#{location.query}" : location.path
          end

          hostname, port = host.split(':')

Integration::Session#host

      # The hostname used in the last request.
      def host
        @host || DEFAULT_HOST
      end
      attr_writer :host

Integration::Session#host=で設定できそう。

Integration::Sessionインスタンスの取得は、ActionDispatch::Integration::Runner#integration_session

      # Reset the current session. This is useful for testing multiple sessions
      # in a single test case.
      def reset!
        @integration_session = Integration::Session.new(app)
      end
      private
        def integration_session
          @integration_session ||= nil
        end

  # API
  config.include RSpec::Rails::RequestExampleGroup, type: :request, file_path: /spec\/api/
  config.include RSpec::Rails::ViewRendering, type: :request, file_path: /spec\/api/
  config.before do
    if self.class.described_class < Grape::API
      reset! unless integration_session
      integration_session.host = 'api.example.com'
    end
  end