5

我有一个表格,我正在使用 Capybara 进行测试。此表单的 URL 会转到我的 Braintree 沙箱,尽管我怀疑任何远程 URL 都会出现问题。当 Capybara 单击表单的提交按钮时,请求被路由到虚拟应用程序而不是远程服务。

这是一个重现此问题的示例应用程序:https ://github.com/radar/capybara_remote 。运行 bundle exec ruby​​ test/form_test.rb 并且测试将通过,这不是我通常所期望的。

为什么会发生这种情况,我可以依赖的这种行为是否总是发生?

4

1 回答 1

12

Mario Visic 在 Capybara 文档中指出了这个描述:

此外,您不能使用 RackTest 驱动程序来测试远程应用程序,或访问您的应用程序可能与之交互的远程 URL(例如,重定向到外部站点、外部 API 或 OAuth 服务)。

但我想知道为什么,所以我开始潜水。这是我的发现:

lib/capybara/node/actions.rb

def click_button(locator)
  find(:button, locator).click
end

我不在乎find这里,因为那行得通。这才是click更有趣的。该方法定义如下:

lib/capybara/node/element.rb

def click
  wait_until { base.click }
end

我不知道是什么,但我看到该方法在andbase中又定义了两次。测试使用的是Selenium 而不是 Selenium,所以它可能是前者:lib/capybara/rack_test/node.rblib/capybara/selenium/node.rbRack::Test

lib/capybara/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/capybara/rack_test/form.rb

def submit(button)
  driver.submit(method, native['action'].to_s, params(button))
end

那好吧。driver可能是Rack::Test水豚的驱动程序。那是在做什么?

lib/capybara/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/capybara/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在该方法的最后一行之前粘贴 a ,并require 'pry'在测试中粘贴 a 。事实证明,method并且:post,为了感兴趣,new_uri它是一个URI带有我们远程表单 URL 的对象。

这种post方法从何而来?method(:post).source_location告诉我:

["/Users/ryan/.rbenv/versions/1.9.3-p374/lib/ruby/1.9.1/forwardable.rb", 199]

这似乎不对……水豚有什么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 aCapybara::RackTest::Browser` 对象。课程开头给出了下一个提示:

class Capybara::RackTest::Browser
  include ::Rack::Test::Methods

我知道这Rack::Test::Methods是有post方法的。是时候潜入那颗宝石了。

库/机架/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呢?

库/机架/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方法,请求被直接路由到这个堆栈,而不是向外发送。

我得出的结论是,这种行为看起来像是故意的,而且我确实可以依靠这种方式。

于 2013-04-15T22:35:01.357 回答