タケユー・ウェブ日報

Ruby on Rails や Flutter といったWeb・モバイルアプリ技術を武器にお客様のビジネス立ち上げを支援する、タケユー・ウェブ株式会社の技術ブログです。

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