想了解下ActionDispatch::IntegrationTest怎样运作,trace一下

class ProductsControllerTest < ActionDispatch::IntegrationTest

  test "should create product" do
    assert_difference('Product.count') do
      binding.trace_tree(html: true, tmp: ['rails', 'test_post_models_url.html']) do
      post products_url, params: { product: @update }
      end
    end

    assert_redirected_to product_url(Product.last)
  end
end


但跑太久了,于是直接去ActionDispatch::IntegrationTest中查找下post方法

毫不意外,它就在actionpack-5.0.2/lib/action_dispatch/testing/integration.rb之中

module ActionDispatch
  module Integration
    module RequestHelpers

      def post(path, *args)
        process_with_kwargs(:post, path, *args)
      end


而RequestHelpers会被Session所include

module ActionDispatch
  module Integration
    class Session
      DEFAULT_HOST = "www.example.com"

      include Minitest::Assertions
      include TestProcess, RequestHelpers, Assertions


post所调的process_with_kwargs也在Session当中

def process_with_kwargs(http_method, path, *args)
  if kwarg_request?(args)
    process(http_method, path, *args)
  else
    non_kwarg_request_warning if args.any?
    process(http_method, path, { params: args[0], headers: args[1] })
  end
end


无论如何,都会调process

def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)

  # ......

  session = Rack::Test::Session.new(_mock_session)

  session.request(build_full_uri(path, request_env), request_env)

  @request_count += 1
  @request = ActionDispatch::Request.new(session.last_request.env)
  response = _mock_session.last_response
  @response = ActionDispatch::TestResponse.from_response(response)
  @response.request = @request
  @html_document = nil
  @url_options = nil

  @controller = @request.controller_instance

  response.status
end


此方法很长,比较值得关注的,是它构建request_env,并传给session.request的部分,这个session本质上是一个经过Rack::MockSession、Rack::Test::Session包裹的@app

def _mock_session
  @_mock_session ||= Rack::MockSession.new(@app, host)
end


@app是ActionDispatch::Integration::Session初始化时塞进来的

def initialize(app)
  super()
  @app = app

  reset!
end


而app其实就是Application对象

From: /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.0.2/lib/action_dispatch/testing/integration.rb @ line 190 ActionDispatch::Integration::Session#initialize:

    186:       def initialize(app)
    187:         super()
    188:         @app = app
    189: binding.pry
 => 190:         reset!
    191:       end

[1] pry(#<#>)> app
=> #<depot::application:0x007f67cd4e1e20< code=""></depot::application:0x007f67cd4e1e20<>


顺便检查下调用栈

[2] pry(#<#>)> _bs_
=> [#<binding:70041918524660 #<class:0x007f67cf57f9c0="">#initialize /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.0.2/lib/action_dispatch/testing/integration.rb:189>,
 #<binding:70041918553200 productscontrollertest#create_session="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" action_dispatch="" testing="" integration.rb:427="">,
 #<binding:70041907797400 productscontrollertest#integration_session="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" action_dispatch="" testing="" integration.rb:409="">,
 #<binding:70041918574500 productscontrollertest#method_missing="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" actionpack-5.0.2="" lib="" action_dispatch="" testing="" integration.rb:486="">,
 #<binding:70041907765920 productscontrollertest#block="" (2="" levels)="" in="" <class:productscontrollertest=""> /home/z/test_rails/depot/test/controllers/products_controller_test.rb:51>,
 #<binding:70041918595940 productscontrollertest#assert_difference="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" activesupport-5.0.2="" lib="" active_support="" testing="" assertions.rb:71="">,
 #<binding:70041918630920 productscontrollertest#block="" in="" <class:productscontrollertest=""> /home/z/test_rails/depot/test/controllers/products_controller_test.rb:50>,
 #<binding:70041918649340 productscontrollertest#block="" (3="" levels)="" in="" run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest="" test.rb:105="">,
 #<binding:70041907683800 productscontrollertest#capture_exceptions="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest="" test.rb:202="">,
 #<binding:70041918669560 productscontrollertest#block="" (2="" levels)="" in="" run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest="" test.rb:102="">,
 #<binding:70041918697060 productscontrollertest#time_it="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest="" test.rb:253="">,
 #<binding:70041918717860 productscontrollertest#block="" in="" run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest="" test.rb:101="">,
 #<binding:70041918710340 productscontrollertest.on_signal="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:349="">,
 #<binding:70041918755080 productscontrollertest#with_info_handler="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest="" test.rb:273="">,
 #<binding:70041918782460 productscontrollertest#run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest="" test.rb:100="">,
 #<binding:70041907600120 minitest.run_one_method="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:822="">,
 #<binding:70041918801980 productscontrollertest.run_one_method="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:323="">,
 #<binding:70041918821760 productscontrollertest.block="" (2="" levels)="" in="" run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:310="">,
 #<binding:70041918847720 productscontrollertest.block="" in="" run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:309="">,
 #<binding:70041907564760 productscontrollertest.on_signal="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:349="">,
 #<binding:70041918866420 productscontrollertest.with_info_handler="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:336="">,
 #<binding:70041918884740 productscontrollertest.run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:308="">,
 #<binding:70041918911000 productscontrollertest.run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" railties-5.0.2="" lib="" rails="" test_unit="" line_filtering.rb:11="">,
 #<binding:70041918938180 minitest.block="" in="" __run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:158="">,
 #<binding:70041907518220 minitest.__run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:158="">,
 #<binding:70041918729260 minitest.run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:135="">,
 #<binding:70041919011440 minitest.run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" railties-5.0.2="" lib="" rails="" test_unit="" minitest_plugin.rb:72="">,
 #<binding:70041902328660 minitest.block="" in="" autorun="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" minitest-5.10.1="" lib="" minitest.rb:62="">,
 #<binding:70041919022500 spring::application#serve="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" spring-2.0.1="" lib="" spring="" application.rb:161="">,
 #<binding:70041919048940 spring::application#block="" in="" run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" spring-2.0.1="" lib="" spring="" application.rb:131="">,
 #<binding:70041919075340 spring::application#run="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" gems="" 2.4.0="" gems="" spring-2.0.1="" lib="" spring="" application.rb:125="">,
 #<binding:70041919092680 object#<top="" (required)=""> /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/spring-2.0.1/lib/spring/application/boot.rb:19>,
 #<binding:70041919117960 object#require="" home="" z="" .rbenv="" versions="" 2.4.0="" lib="" ruby="" 2.4.0="" rubygems="" core_ext="" kernel_require.rb:55="">,
 #<binding:70041919135200 object#<main=""> -e:1>]</binding:70041919135200></binding:70041919117960></binding:70041919092680></binding:70041919075340></binding:70041919048940></binding:70041919022500></binding:70041902328660></binding:70041919011440></binding:70041918729260></binding:70041907518220></binding:70041918938180></binding:70041918911000></binding:70041918884740></binding:70041918866420></binding:70041907564760></binding:70041918847720></binding:70041918821760></binding:70041918801980></binding:70041907600120></binding:70041918782460></binding:70041918755080></binding:70041918710340></binding:70041918717860></binding:70041918697060></binding:70041918669560></binding:70041907683800></binding:70041918649340></binding:70041918630920></binding:70041918595940></binding:70041907765920></binding:70041918574500></binding:70041907797400></binding:70041918553200></binding:70041918524660>


method_missing的出现令人有点疑惑,整理一下,意思是,IntegrationTest是一个Runner,当调用什么assert_xxx、post、products_url时,如果没这个方法定义,就委托到integration_session(通常就是assert_xxx之外的那些方法)

class IntegrationTest < ActiveSupport::TestCase

  module Behavior
    extend ActiveSupport::Concern

    include Integration::Runner
  end

  include Behavior

  module Runner
    include ActionDispatch::Assertions

    APP_SESSIONS = {}

    attr_reader :app

    def initialize(*args, &blk)
      super(*args, &blk)
      @integration_session = nil
    end

    def method_missing(sym, *args, &block)
      if integration_session.respond_to?(sym)
        integration_session.__send__(sym, *args, &block).tap do
          copy_session_variables!
        end
      else
        super
      end
    end
  end
end


根据调用栈来看,integration_session会在第一次调用时生成,所用的类是动态生成的Integration::Session子类

def create_session(app)
  klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
    # If the app is a Rails app, make url_helpers available on the session
    # This makes app.url_for and app.foo_path available in the console
    if app.respond_to?(:routes)
      include app.routes.url_helpers
      include app.routes.mounted_helpers
    end
  }
  klass.new(app)
end


为什么要使用Rack::MockSession、Rack::Test::Session来包装application呢?先查找下他们定义在哪里——原来是有专门的一个叫rack-test的gem的

[20] pry(#<#>)> m = Rack::Test::Session; m.instance_methods(false).map{|meth| src = m.instance_method(meth).source_location and src[0]}.uniq
=> ["/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-test-0.6.3/lib/rack/test.rb", "/home/z/.rbenv/versions/2.4.0/lib/ruby/2.4.0/forwardable.rb"]
[21] pry(#<#>)> m = Rack::MockSession; m.instance_methods(false).map{|meth| src = m.instance_method(meth).source_location and src[0]}.uniq
=> ["/home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-test-0.6.3/lib/rack/mock_session.rb"]


看看源码及注释就知道,它主要就是为了模拟与client的cookie的交互

首先,所有get、post等等(从刚才Runner的method_missing发来)都会调process_request,然后process_request再委托到mock_session

module Rack
  module Test

    # This class represents a series of requests issued to a Rack app, sharing
    # a single cookie jar
    #
    # Rack::Test::Session's methods are most often called through Rack::Test::Methods,
    # which will automatically build a session when it's first used.
    class Session
      extend Forwardable
      include Rack::Test::Utils

      def_delegators :@rack_mock_session, :clear_cookies, :set_cookie, :last_response, :last_request

      def initialize(mock_session)
        @headers = {}
        @env = {}

        if mock_session.is_a?(MockSession)
          @rack_mock_session = mock_session
        else
          @rack_mock_session = MockSession.new(mock_session)
        end

        @default_host = @rack_mock_session.default_host
      end

      def get(uri, params = {}, env = {}, &block)
        env = env_for(uri, env.merge(:method => "GET", :params => params))
        process_request(uri, env, &block)
      end

      def process_request(uri, env)
        uri = URI.parse(uri)
        uri.host ||= @default_host

        @rack_mock_session.request(uri, env)

        if retry_with_digest_auth?(env)
          auth_env = env.merge({
            "HTTP_AUTHORIZATION"          => digest_auth_header,
            "rack-test.digest_auth_retry" => true
          })
          auth_env.delete('rack.request')
          process_request(uri.path, auth_env)
        else
          yield last_response if block_given?

          last_response
        end
      end


而mock_session则会将下层application所返回的header里的"Set-Cookie"抽出,放到Rack::Test::CookieJar中,并且在一次IntegrationTest中,永远都只用这个cookie_jar,相当于把client推前到了这个Rack::MockSession栈帧中,对于请求来说,就是每次请求都先把上次塞入cookie_jar的东西取出放到env["HTTP_COOKIE"],才传给下层application

module Rack

  class MockSession
    attr_writer :cookie_jar
    attr_reader :default_host

    def initialize(app, default_host = Rack::Test::DEFAULT_HOST)
      @app = app
      @after_request = []
      @default_host = default_host
      @last_request = nil
      @last_response = nil
    end

    def request(uri, env)
      env["HTTP_COOKIE"] ||= cookie_jar.for(uri)
      @last_request = Rack::Request.new(env)
      status, headers, body = @app.call(@last_request.env)

      @last_response = MockResponse.new(status, headers, body, env["rack.errors"].flush)
      body.close if body.respond_to?(:close)

      cookie_jar.merge(last_response.headers["Set-Cookie"], uri)

      @after_request.each { |hook| hook.call }

      if @last_response.respond_to?(:finish)
        @last_response.finish
      else
        @last_response
      end
    end

    def cookie_jar
      @cookie_jar ||= Rack::Test::CookieJar.new([], @default_host)
    end


在刚才的binding.pry中,需exit 7次才跑完整个IntegrationTest,刚好与srcaffold生成的7个test对应,即每个test都有一个全新的session(不过用的都是同一个Application对象)