Rails中用JavaScript测试代码覆盖率的方法

267 阅读6分钟

在现代应用程序中,用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 文件夹中)。

image.png

和JavaScript覆盖率(存储在js-coverage 文件夹中)

image.png

故障排除

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代码覆盖率,我们可以找到那些我们没有测试但用户直接与之交互的地方。这样我们就可以改进我们的测试,使我们的代码库更加健壮,同时也使我们在需要重构代码或添加任何新功能时更加自信。