我有一张表格,我正在使用Capybara进行测试。这个表单的URL转到我的Braintree沙箱,虽然我怀疑任何远程URL都会出现问题。当Capybara单击表单的提交按钮时,请求将路由到虚拟应用程序而不是远程服务。
以下是重现此问题的示例应用:https://github.com/radar/capybara_remote。运行bundle exec ruby test / form_test.rb,测试将通过,这不是我通常所期望的。
为什么会发生这种情况,这种行为是我可以依赖的?
答案 0 :(得分:12)
Mario Visic在Capybara文档中指出了这个描述:
此外,您无法使用RackTest驱动程序测试远程应用程序,也无法访问应用程序可能与之交互的远程URL(例如,重定向到外部站点,外部API或OAuth服务)。
但我想知道为什么,所以我来源潜水。这是我的发现:
<强> LIB /水豚/节点/ actions.rb 强>
def click_button(locator)
find(:button, locator).click
end
我不关心这里的find
,因为那是有效的。这是click
更有趣。该方法的定义如下:
<强> LIB /水豚/节点/ element.rb 强>
def click
wait_until { base.click }
end
我不知道base
是什么,但我看到该方法在lib/capybara/rack_test/node.rb
和lib/capybara/selenium/node.rb
中定义了两次。测试使用的是Rack::Test
而不是Selenium,所以它可能是前者:
<强> LIB /水豚/ rack_test / node.rb 强>
def click
if tag_name == 'a'
method = self["data-method"] if driver.options[:respect_data_method]
method ||= :get
driver.follow(method, self[:href].to_s)
elsif (tag_name == 'input' and %w(submit image).include?(type)) or
((tag_name == 'button') and type.nil? or type == "submit")
Capybara::RackTest::Form.new(driver, form).submit(self)
end
end
tag_name
可能不是链接 - 因为它是我们点击的按钮 - 所以它落在elsif
。它绝对是input
标记type == "submit"
,所以让我们看看Capybara::RackTest::Form
做了什么:
<强> LIB /水豚/ rack_test / form.rb 强>
def submit(button)
driver.submit(method, native['action'].to_s, params(button))
end
好的。 driver
可能是Capybara的Rack::Test
驱动程序。那是做什么的?
<强> LIB /水豚/ rack_test / driver.rb 强>
def submit(method, path, attributes)
browser.submit(method, path, attributes)
end
这个神秘的浏览器是什么?谢天谢地,它在同一个文件中定义:
def browser
@browser ||= Capybara::RackTest::Browser.new(self)
end
让我们看看这个类的submit
方法做了什么。
<强> LIB /水豚/ rack_test / browser.rb 强>
def submit(method, path, attributes)
path = request_path if not path or path.empty?
process_and_follow_redirects(method, path, attributes, {'HTTP_REFERER' => current_url})
end
process_and_follow_redirects
按照包装上的内容执行操作:
def process_and_follow_redirects(method, path, attributes = {}, env = {})
process(method, path, attributes, env)
5.times do
process(:get, last_response["Location"], {}, env) if last_response.redirect?
end
raise Capybara::InfiniteRedirectError, "redirected more than 5 times, check for infinite redirects." if last_response.redirect?
end
process
:
def process(method, path, attributes = {}, env = {})
new_uri = URI.parse(path)
method.downcase! unless method.is_a? Symbol
if new_uri.host
@current_host = "#{new_uri.scheme}://#{new_uri.host}"
@current_host << ":#{new_uri.port}" if new_uri.port != new_uri.default_port
end
if new_uri.relative?
if path.start_with?('?')
path = request_path + path
elsif not path.start_with?('/')
path = request_path.sub(%r(/[^/]*$), '/') + path
end
path = current_host + path
end
reset_cache!
send(method, path, attributes, env.merge(options[:headers] || {}))
end
时间打破调试器,看看method
在这里。在该方法的最后一行之前粘贴binding.pry
,并在测试中粘贴require 'pry'
。结果method
为:post
,为了感兴趣,new_uri
是一个URI
对象,带有我们的远程表单的网址。
这个post
方法来自哪里? method(:post).source_location
告诉我:
["/Users/ryan/.rbenv/versions/1.9.3-p374/lib/ruby/1.9.1/forwardable.rb", 199]
这似乎不对...... Capybara在某处有def post
吗?
capybara (master)★ack "def post"
lib/capybara/rack_test/driver.rb
76: def post(*args, &block); browser.post(*args, &block); end
冷却。我们知道browser is a
Capybara :: RackTest :: Browser`对象。课程开头给出了下一个提示:
class Capybara::RackTest::Browser
include ::Rack::Test::Methods
我知道Rack::Test::Methods
附带post
方法。是时候潜入那颗宝石了。
<强> LIB /齿条/ test.rb 强>
def post(uri, params = {}, env = {}, &block)
env = env_for(uri, env.merge(:method => "POST", :params => params))
process_request(uri, env, &block)
end
暂时忽略env_for
,process_request
做了什么?
<强> LIB /齿条/ test.rb 强>
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
嘿,@rack_mock_session
看起来很有趣。那定义在哪里?
rack-test (master)★ack "@rack_mock_session ="
lib/rack/test.rb
40: @rack_mock_session = mock_session
42: @rack_mock_session = MockSession.new(mock_session)
在两个地方,彼此非常接近。这些线上和周围有什么?
def initialize(mock_session)
@headers = {}
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
好的,所以它确保它是一个MockSession
对象。什么是MockSession
以及如何定义request
方法?
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)
headers["Referer"] = env["HTTP_REFERER"] || ""
@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
我将在这里继续前进并假设@app
是Rack应用程序堆栈。通过调用call
方法,请求将直接路由到此堆栈,而不是走向世界。
我得出结论,这种行为看起来像是故意的,我确实可以依赖它。