ActionDispatch::IntegrationTest基本运作
想了解下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对象)