在现代应用程序中,用JavaScript增强用户体验是很常见的。无论它只是在这里和那里撒一些JavaScript,还是一个完全基于JS的前端,当涉及到应用程序的正确功能时,这和你的Ruby代码一样重要。在这篇文章中,我们将展示如何在运行系统/集成测试时测量JavaScript代码的测试代码覆盖率,以及Ruby代码的覆盖率。
示例应用程序
我们使用React、TypeScript和Vanilla JavaScript代码创建了一个样本应用程序,以及RSpec和MiniTest两个例子,你可以用来参考。这是存储库github.com/fastruby/js…
要求
为了能够测量JavaScript代码覆盖率,我们将使用Istanbul仪器库,这意味着本指南只有在你使用支持它的JavaScript捆绑器(例如Webpacker)的情况下才能工作,这在Sprokets上是不可行的。
仪表
JS代码
为了测量哪些代码被执行,我们需要对代码进行检测。为此,我们将使用伊斯坦布尔库,它负责跟踪每一行的执行情况,并将这些信息存储在window 对象中,作为window.__coverage__ 。
我们必须用yarn add istanbul-instrumenter-loader --dev 添加伊斯坦布尔加载器的节点包,然后配置webpack以使用所需文件的加载器。
// config/webpack/environment.js
const { environment } = require("@rails/webpacker");
if (process.env.RAILS_ENV === "test") {
environment.loaders.append("istanbul-instrumenter", {
test: /(\.js)$|(\.jsx)$|(\.ts)$|(\.tsx)$/,
use: {
loader: "istanbul-instrumenter-loader",
options: { esModules: true },
},
enforce: "post",
exclude: /node_modules/,
});
}
module.exports = environment;
注意,我们是在通用的
environment.js文件中添加配置,而不是在test.js文件中。至少在我们的测试中,test.js文件似乎在运行测试时不会被执行。
如果你想使用DevTools控制台看到
window.__coverage__的信息,你可以在test的环境中运行Rails应用程序,并使用RAILS_ENV=test NODE_ENV=test rails s。
Ruby代码
对于Ruby代码覆盖,我们将使用Simplecov。
首先,我们必须用gem 'simplecov', require: false ,将gem添加到我们的gemfile中。
然后,我们更新测试环境配置。
# config/environment/test.rb
if ENV["RAILS_ENV"] == "test" && ENV["COVERAGE"]
require "simplecov"
end
而我们将使用一个.simplecov 文件,其中有这个内容。
# .simplecov
SimpleCov.start "rails"
我们这样启动SimpleCov是因为我们以后会在这个配置文件中添加更多的东西。
每次测试后的JS覆盖数据
当Rails运行我们的系统测试时,它会打开一个新的浏览器窗口并应用我们用Capybara编码的动作。在该测试运行期间,伊斯坦布尔将在每个例子的执行过程中跟踪window.__coverage__ 对象中的执行行。这些信息只有在每次测试期间才能得到,所以我们需要在每个例子运行后提取这些信息。
为了存储每个测试后的结果,我们在每个系统测试后运行一个代码块,我们从当前测试的窗口对象中获取数值,并将其存储在一个特殊的.nyc_output 目录中。
首先,我们必须在config/environments/test.rb 文件的顶部用这个更新我们的测试环境配置。
# config/environment/test.rb
# if running tests and we want the code coverage, include `simplecov` and prepare the directories
if ENV["RAILS_ENV"] == "test" && ENV["COVERAGE"]
require "simplecov"
FileUtils.mkdir_p(".nyc_output") # make sure the directory exists
Dir.glob("./.nyc_output/*").each{ |f| FileUtils.rm(f) } # clear results from previous runs
end
# this function should be executed when we want to store the current `window.__coverage__` info in a file
def dump_js_coverage
return unless ENV["COVERAGE"]
page_coverage = page.evaluate_script("JSON.stringify(window.__coverage__);")
return if page_coverage.blank?
# we will store one `js-....json` file for each system test, and we save all of them in the .nyc_output dir
File.open(Rails.root.join(".nyc_output", "js-#{Random.rand(10000000000)}.json"), "w") do |report|
report.puts page_coverage
end
end
然后我们必须附加一个钩子,在每个系统测试结束后执行该函数。根据你使用的测试运行器,这是在不同的地方完成的。
对于MiniTest来说
# test/application_system_test_case.rb
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
def before_teardown
dump_js_coverage
end
end
如果你使用其他类型的测试与JavaScript(如功能测试或集成测试),你可能想把before_teardown钩子也添加到这些类型。
对于RSpec
# spec/rails_helper.rb
RSpec.shared_context "dump JS coverage" do
after { dump_js_coverage }
end
RSpec.configure do |config|
config.include_context "dump JS coverage", type: :system
...
注意,这是我们发现的一个问题的变通方法。理想情况下,你会在这里使用
config.after(:each, type: :system) do块,但after钩子被触发得太晚了,所以我们使用这个变通方法,使用共享上下文运行dump_js_coverage。
还要注意的是,我们在例子的最后收集测试覆盖率,提取
window.__coverage__对象的信息。如果在一个例子中,你再次调用visit,或者做一个清除该信息的动作,那么在例子结束时就不会完成(如果你使用像Turbo或Turbolinks这样的东西,应该没有问题,因为它将用ajax请求代替标准HTTP请求,在页面变化之间保持__coverage__对象)。如果你的测试碰巧改变了页面,你失去了覆盖面,你可能需要找到一些变通的方法(比如在调用
visit方法之前手动添加对dump_js_coverage的调用,或者在调用点击click_link方法或触发表单提交的动作时添加一个包装器)。在本文中,有太多的边缘案例,无法涵盖这些。
所有测试后的JS覆盖数据合并
在我们完成所有的测试后,我们必须将所有的单一测试结果合并到一个JS报告文件中。为了管理这些报告,我们将使用伊斯坦布尔的nyc 命令。
首先,我们必须使用yarn add nyc --dev 来安装它。
然后,我们用这个内容添加一个.nycrc 配置文件。
{
"report-dir": "js-coverage"
}
这个文件将告诉nyc将生成的报告放在哪里。
现在,我们必须在package.json文件中添加脚本。
"scripts": {
"coverage": "nyc merge .nyc_output .nyc_output/out.json & nyc report --reporter=html"
}
这个脚本做两件事。
- 它将.nyc_output下的所有文件合并为一个
out.json文件 - 然后用这个生成的文件生成一个HTML报告。
有了这个脚本,我们就可以运行yarn coverage ,在js-coverage 文件夹中生成一个HTML报告。
最后,我们必须在测试套件执行结束时运行这个脚本。要做到这一点,我们将使用Simplecov的at_exit 块来运行yarn coverage 脚本。我们可以用这个内容更新.simplecov 文件。
# .simplecov
SimpleCov.at_exit do
# process simplecov ruby report
# this is the default if no `at_exit` block is configured
SimpleCov.result.format!
# process istanbul js report
system("yarn coverage")
end
SimpleCov.start "rails"
报告的内容
在所有测试执行完毕后,它同时生成了Ruby覆盖率(存储在coverage 文件夹中)。
和JavaScript覆盖率(存储在js-coverage 文件夹中)
故障排除
Simplecov和并行化
Simplecov 在计算代码覆盖率时有一些已知的问题,因为测试是并行运行的,这是 MiniTest github.com/simplecov-r… 的默认情况。
如果我们想获得Ruby测试的覆盖率,一个变通的办法是禁用并行化。
# test/test_helper.rb
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
class ActiveSupport::TestCase
# Run tests in parallel with specified workers
workers = ENV["COVERAGE"] ? 1 : :number_of_processors
parallelize(workers: workers)
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
end
最后提示
在你的版本控制中忽略一些目录是一个好主意,这样你就不会推送你的覆盖率报告。在你的.gitignore文件中添加这些行。
.nyc_output
coverage
js-coverage
结论
代码覆盖率指标不是一个好的测试套件的直接指标,但它是一个很好的指标,可以显示你的代码的哪些部分没有被测试强调,以指导你哪些功能没有被测试。通过JavaScript代码覆盖率,我们可以找到那些我们没有测试但用户直接与之交互的地方。这样我们就可以改进我们的测试,使我们的代码库更加健壮,同时也使我们在需要重构代码或添加任何新功能时更加自信。