当我们运行测试时,在大多数情况下,我们不希望碰到外部服务,所以我们的测试不依赖于外部服务,而且更加稳定。我们可以使用像VCR或WebMock这样的宝石来存根Rails应用程序所做的请求,但当请求是由JavaScript代码发起时......那就是另一回事了。
两种不同的方法
在这两种情况下,我们运行的测试都使用Capybara和Selenium-webdriver宝石。这是创建一个新的Rails应用程序时的默认做法,所以你应该在你的Gemfile中已经有了它们。测试的类型可以是feature,integration,system 等,因为只要测试的类型使用了 Selenium 和 Capybara,我们就可以代理或拦截这些请求。
代理请求
使用这种方法,浏览器开始时有一个代理配置,指向一个本地代理,它将捕捉任何请求,并让我们处理不同的请求。从浏览器的角度来看,请求是正常进行的,拦截是由代理服务器处理的。我们不会在这篇博文中探讨这种方法,但如果你想采用这种解决方案,你可以试试Hashrocket的capybara-webmockgem。
拦截请求
当使用这种方法时,我们将使用devtools的一些功能在浏览器中直接拦截浏览器的请求,而驱动程序将在任何时候在我们的Ruby代码中执行一个回调。在这种情况下,浏览器正在拦截请求,而驱动程序正在运行我们的Ruby代码以生成响应(或让它继续)。
用Selenium拦截
Selenium-webdriver第4版引入了一个新功能,允许添加驱动扩展,用HasNetworkInterception模块拦截网络请求。所以我们需要在Gemfile中指定版本。
gem 'selenium-webdriver', '>= 4.0'
拦截器模块
我们创建了一个小型的拦截器模块,可以与RSpec和Minitest一起使用。我们可以根据所使用的测试工具,将该文件复制到spec/support/interceptor.rb 或test/support/interceptor.rb 。
Selenium-devtools
为了让HasNetworkInterception 扩展工作,我们也需要添加selenium-devtoolsgem。
# Gemfile
group :test do
# Adds support for Capybara system testing and selenium driver
gem 'capybara'
gem 'selenium-webdriver', '>= 4.0'
gem 'selenium-devtools'
# Easy installation and use of web drivers to run system tests with browsers
gem 'webdrivers'
end
MiniTest设置
在test/application_system_test_case.rb 文件中,我们必须要求该模块并将其包含在ApplicationSystemTestCase 类中。然后我们必须使用生命周期钩子添加一些代码。
# test/application_system_test_case.rb
require "test_helper"
require_relative "support/interceptor"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
include Interceptor
driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
def after_setup
start_intercepting
super
end
def before_teardown
stop_intercepting
super
end
end
RSpec设置
在spec/rails_helper.rb 文件中,我们必须要求该模块并将其包含在system (或feature ,如果你使用这些)类型的规格中。然后我们必须使用生命周期钩子添加一些代码。
# spec/rails_helper.rb
require "spec_helper"
...
...
require_relative "support/interceptor"
RSpec.configure do |config|
config.include(Interceptor, type: :system)
config.before(:each, type: :system) do
# this `driven_by` call is needed because of this issue in rspec-rails
# https://github.com/rspec/rspec-rails/issues/2550
# the `drive_by` method is valid only for system tests, ignore this fix if type is `feature`
driven_by Capybara.javascript_driver
start_intercepting
end
config.after(:each, type: :system) do
stop_intercepting
end
如果你使用其他类型的可能使用Capybara和Selenium的测试,如
feature或integration测试,你可以做类似的设置。
拦截请求
现在我们可以使用intercept 方法来为特定的URL设置固定的响应。
例如,如果我们有一个带有按钮的页面,上面写着Make SWAPI request ,它向《星球大战》的API做了一个JavaScript请求,最后把响应放在一个id为response 的div中,我们可以这样测试。
# test/system/index_test.rb
require "application_system_test_case"
class IndexTest < ApplicationSystemTestCase
test "The Star Wars API request" do
visit root_path
intercept("https://swapi.dev/api/planets/1/", "my mocked response")
click_button "Make SWAPI request"
assert_selector "#response", text: "my mocked response"
end
当浏览器进行外部请求时,selenium扩展将执行由我们的拦截器模块定义的回调,并将对这个特定的URL进行响应,用固定的字符串代替对外部API的真正请求。
同样的方法也可以在RSpec规范中使用。
配置拦截器
默认的拦截方式
在很多页面中都有一些全局性的JavaScript做外部请求(比如分析代码),使用一些外部CDN(用于网页字体或图标)或外部widget(比如Twitter或Facebook的),这都是很常见的事情。我们可以定义默认情况下任何测试都会使用的拦截方式,这样我们就不必记得为每个测试拦截它们。
我们可以通过重写default_interceptions 方法来做到这一点。假设我们想在测试中拦截任何对Google的请求(Maps, WebFonts, Analytics, etc)。
module Interceptor
def default_interceptions
[{url: /google.*\.com/, method: :any, response: ""}]
end
end
检查
interceptor.rb文件中的注释,了解有效值。
注意,拦截器模块会拦截任何默认情况下没有明确允许的请求。重写default_interceptions有助于明确这一点,并允许设置预期响应,而不仅仅是一个空字符串。但是仅仅通过调用
start_intercepting方法,我们就可以阻止任何外部请求。
允许的请求
默认情况下,任何没有明确拦截或允许的外部请求都会被拦截,并给出一个空的响应,并记录到控制台。如果测试恰好依赖于这个外部端点,测试将失败,我们可以添加一个显式拦截或允许它。如果测试不依赖于外部端点,我们可以通过不做那个不必要的请求来自由改进。
我们可以通过重写allowed_requests 方法来改变这一点。如果我们有一个CDN服务器,里面有重要的资产(比如一个JavaScript库),我们可以允许这个外部请求。
module Interceptor
def allowed_requests
[%r{http://#{Capybara.server_host}}, {url: my_cdn_domain, method: :get}]
end
end
检查
interceptor.rb文件中的注释,了解有效值。
请注意,你可能总是希望Capybara.server_host url被允许。
总结
通过在一些项目中添加这个拦截器,我们发现在测试过程中触发了许多不必要的外部请求(字体、图标、小工具、分析)。通过检查日志,我们能够识别它们,并添加适当的拦截规则来明确。这减少了测试期间使用的带宽,也减少了Capybara开始与页面互动的响应时间。
主要的好处是,我们可以找到一些依赖外部端点的测试,如果外部端点无法到达,测试最终会失败,或者如果外部端点当时工作缓慢,测试也会缓慢。通过拦截这些请求,我们可以有一个更强大的测试套件,并提高测试的质量。
最后,我们用这个设置创建了一个示例应用程序。