Skip to main content
Cypress应用

在Cypress中测量代码覆盖率

info
你将学到
  • 代码覆盖率与Cypress UI覆盖率的区别
  • 如何为应用程序代码添加检测以测量代码覆盖率
  • 如何从Cypress测试中收集代码覆盖率数据
  • 如何合并并行测试的代码覆盖率
  • 如何收集不同类型测试的代码覆盖率

介绍

随着编写的端到端测试越来越多,你可能会思考: 我需要编写更多测试吗?应用程序中是否还有未测试的部分?是否有部分代码被过度测试了?

Cypress提供了几种解决方案来回答这些问题:

代码覆盖率

代码覆盖率是一个指标,帮助你了解测试覆盖了多少应用程序代码。如果应用程序中有重要逻辑部分未被测试执行,那么可以添加新测试来确保这些部分的逻辑也被测试到。

通过代码覆盖率计算测试期间执行的源代码行数。代码覆盖率需要在运行代码前通过插桩在源代码中插入额外的计数器。本文档将重点介绍代码覆盖率及设置所需的插桩。

UI覆盖率

UI覆盖率是基于应用程序交互元素的视觉测试覆盖率报告。它直接在应用程序的每个页面中突出显示未测试到的区域,帮助你做出数据驱动的测试决策。

UI覆盖率还能与Cypress测试无缝集成,基于已编写的测试构建——无需像代码覆盖率那样进行额外插桩。

要了解更多关于UI覆盖率的信息,请参阅我们的UI覆盖率指南

UI覆盖率演示展示Cloud产品的UI

代码插桩

插桩将如下代码...

add.js
function add(a, b) {
return a + b
}
module.exports = { add }

...解析并找到所有函数、语句和分支,然后在代码中插入计数器。对于上述代码,插桩后可能如下:

// 此对象统计每个函数和语句的执行次数
const c = (window.__coverage__ = {
// "f"统计每个函数的调用次数
// 源代码中只有一个函数,因此以[0]开始
f: [0],
// "s"统计每个语句的调用次数
// 有3个语句,都以0开始
s: [0, 0, 0],
})

// 原始代码 + 增量语句
// 使用"c"作为"window.__coverage__"对象的别名
// 第一个语句定义函数,增加计数器
c.s[0]++
function add(a, b) {
// 函数被调用,然后执行第二个语句
c.f[0]++
c.s[1]++

return a + b
}
// 即将执行第三个语句
c.s[2]++
module.exports = { add }

假设我们从测试文件中加载上述插桩后的源文件。一些计数器会立即增加!

add.cy.js
const { add } = require('./add')
// JavaScript引擎已解析并评估"add.js"源代码
// 这运行了一些增量语句
// __coverage__现在有
// f: [0] - 函数"add"未被调用
// s: [1, 0, 1] - 第一个和第三个计数器增加了
// 但函数"add"内的语句未被调用

我们希望确保文件add.js中的每个语句和函数至少被测试执行一次。因此我们编写一个测试:

add.cy.js
const { add } = require('./add')

it('adds numbers', () => {
expect(add(2, 3)).to.equal(5)
})

当测试调用add(2, 3)时,"add"函数内的计数器增加,覆盖率对象变为:

{
// "f"记录每个函数的调用次数
// 源代码中只有一个函数
// 因此以[0]开始
f: [1],
// "s"记录每个语句的调用次数
// 有3个语句,都以0开始
s: [1, 1, 1]
}

这个单一测试实现了100%的代码覆盖率——每个函数和语句至少被执行了一次。但在实际应用中,实现100%代码覆盖率需要多个测试。

测试完成后,覆盖率对象可以被序列化并保存到磁盘,以便生成人类友好的报告。收集的覆盖率信息也可以发送到外部服务,帮助进行拉取请求审查。

info

如果你不熟悉代码覆盖率或想了解更多,可以查看"理解JavaScript代码覆盖率"博客文章第一部分第二部分

Cypress不会对你的代码进行插桩——你需要自己完成。JavaScript代码插桩的黄金标准是久经考验的Istanbul,幸运的是,它与Cypress配合得很好。你可以通过以下两种方式之一在构建步骤中对代码进行插桩:

使用NYC

要对src文件夹中的应用程序代码进行插桩并将结果保存在instrumented文件夹中,使用以下命令:

npx nyc instrument --compact=false src instrumented

我们传递--compact=false标志以生成人类友好的输出。

插桩将你的原始代码片段...

src/index.js
const store = createStore(reducer)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

...包装每个语句,添加计数器以跟踪JavaScript运行时执行了多少次每个源代码行。

const store = (cov_18hmhptych.s[0]++, createStore(reducer))
cov_18hmhptych.s[1]++
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

注意cov_18hmhptych.s[0]++cov_18hmhptych.s[1]++的调用,它们增加了语句计数器。所有计数器和额外的簿记信息都存储在附加到浏览器window对象的单个对象中。如果我们提供instrumented文件夹而不是src并打开应用程序,可以看到这些计数器。

代码覆盖率对象

如果我们深入覆盖率对象,可以看到每个文件中执行的语句。例如文件src/index.js有以下信息:

索引文件中覆盖的语句计数器

绿色高亮显示了该文件中的4个语句。前三个语句各执行了一次,最后一个语句从未执行(可能在一个if语句内)。通过使用应用程序,我们可以增加计数器并将一些零计数器变为正数。

使用代码转译管道

除了使用npx instrument命令,我们可以使用babel-plugin-istanbul在转译过程中对代码进行插桩。将此插件添加到.babelrc文件中。

.babelrc
{
"presets": ["@babel/preset-react"],
"plugins": ["transform-class-properties", "istanbul"]
}

现在我们可以提供应用程序并获得插桩后的代码,无需中间文件夹,但结果是相同的插桩代码被浏览器加载,相同的window.__coverage__对象跟踪原始语句。

info

查看@cypress/code-coverage#examples获取展示不同代码覆盖率设置的全示例项目。

打包代码和源映射原始文件

nycbabel-plugin-istanbul的一个非常好的特性是自动生成源映射,允许我们收集代码覆盖率信息,同时在开发者工具中与原始非插桩代码交互。在上面的截图中,打包文件(绿色箭头)有覆盖率计数器,但绿色矩形中的源映射文件显示原始代码。

info

nycbabel-plugin-istanbul只对应用程序代码进行插桩,不对node_modules中的第三方依赖进行插桩。

E2E代码覆盖率

为了处理每个测试期间收集的代码覆盖率,有一个@cypress/code-coverageCypress插件。它合并每个测试的覆盖率并保存组合结果。它还调用nyc(其peer依赖)生成供人类阅读的静态HTML报告。

安装插件

info

请查阅@cypress/code-coverage文档获取最新的安装说明。

npm install @cypress/code-coverage --save-dev

然后将以下代码添加到supportFilesetupNodeEvents函数中。

// cypress/support/e2e.js
import '@cypress/code-coverage/support'
const { defineConfig } = require('cypress')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
require('@cypress/code-coverage/task')(on, config)
// 包含任何其他插件代码...

// 返回带有任何更改的环境变量的配置对象非常重要
return config
},
},
})

现在运行Cypress测试时,你应该在测试完成后看到几个命令。我们用绿色矩形高亮了这些命令。

覆盖率插件命令

测试完成后,最终的代码覆盖率被保存到.nyc_output文件夹。这是一个JSON文件,我们可以从中生成多种格式的报告。@cypress/code-coverage插件自动生成HTML报告——测试完成后可以在本地打开coverage/index.html页面。你也可以调用nyc report生成其他报告,例如将覆盖率信息发送到第三方服务。

查看代码覆盖率摘要

要在测试运行后查看代码覆盖率摘要,运行以下命令。

npx nyc report --reporter=text-summary

========= 覆盖率摘要 =======
语句 : 76.3% ( 103/135 )
分支 : 65.31% ( 32/49 )
函数 : 64% ( 32/50 )
行数 : 81.42% ( 92/113 )
==================================
info

**提示:**将coverage文件夹作为构建工件存储在持续集成服务器上。因为报告是静态HTML页面,一些CI可以直接从其Web应用程序中显示它。下面的截图显示了存储在CircleCI上的覆盖率报告。点击index.html可以直接在浏览器中查看报告。

CircleCI上的覆盖率HTML报告

代码覆盖率作为指南

即使是一个测试也可以覆盖大量应用程序代码。例如,让我们运行以下测试,添加几个项目,然后将其中一个标记为完成。

it('添加并完成任务', () => {
cy.visit('/')
cy.get('.new-todo').type('write code{enter}')
cy.get('.new-todo').type('write tests{enter}')
cy.get('.new-todo').type('deploy{enter}')

cy.get('.todo').should('have.length', 3)

cy.get('.todo').first().find('.toggle').check()

cy.get('.todo').first().should('have.class', 'completed')
})

运行测试并打开HTML报告后,我们看到应用程序的代码覆盖率为76%。

单个测试后的覆盖率报告

更好的是,我们可以深入单个源文件,查看遗漏了哪些代码。在我们的示例应用程序中,主要状态逻辑在src/reducers/todos.js文件中。让我们看看这个文件的代码覆盖率:

主应用程序逻辑覆盖率

注意ADD_TODO动作执行了3次——因为我们的测试添加了3个待办事项,而COMPLETE_TODO动作只执行了1次——因为我们的测试将1个待办事项标记为完成。

未覆盖的源代码行,用黄色(测试遗漏的switch cases)和红色(常规语句)标记,是编写更多端到端测试的绝佳指南。我们需要测试删除待办事项、编辑它们、将所有待办事项标记为完成以及清除已完成项目的测试。当我们覆盖src/reducers/todos.js中的每个switch语句时,我们可能会接近100%的代码覆盖率。更重要的是,我们将覆盖用户预期使用的主要应用程序功能。

我们可以编写更多E2E测试。

Cypress通过更多测试

生成的HTML报告显示99%的代码覆盖率

99%代码覆盖率

除了一个源文件外,所有源文件都达到了100%覆盖率。我们可以对我们的应用程序充满信心,并在知道我们有一套强大的端到端测试的情况下安全地重构代码。

如果可能,我们建议在Cypress功能测试之外实现视觉测试,以避免CSS和视觉回归。

合并并行测试的代码覆盖率

如果你在并行执行Cypress测试,每台机器最终都会得到一个仅显示部分执行代码的代码覆盖率报告。通常外部代码覆盖率服务会为你合并这些部分报告。如果你想自己合并报告:

  • 在每台运行Cypress测试的机器上,将生成的代码覆盖率报告复制到公共文件夹下的唯一名称中,以避免覆盖
  • 所有E2E测试完成后,使用nyc merge命令自己合并报告

你可以在我们的cypress-io/cypress-example-conduit-app中找到合并部分报告的示例。

E2E和单元代码覆盖率

让我们看看那个有一个"遗漏"行的文件。它是src/selectors/index.js文件,如下所示。

选择器文件中有一个未被端到端测试覆盖的行

未被端到端测试覆盖的源代码行显示了一个UI无法访问的边缘情况。但这个switch case绝对值得测试——至少为了避免在未来重构期间意外改变其行为。

我们可以通过从Cypress规范文件中导入getVisibleTodos函数直接测试这段代码。本质上,我们使用Cypress作为单元测试工具(更多单元测试配方请见这里)。

以下是我们确认抛出错误的测试。

// cypress/e2e/selectors.cy.js
import { getVisibleTodos } from '../../src/selectors'

describe('getVisibleTodos', () => {
it('为未知的可见性过滤器抛出错误', () => {
expect(() => {
getVisibleTodos({
todos: [],
visibilityFilter: 'unknown-filter',
})
}).to.throw()
})
})

测试通过,即使没有访问Web应用程序。

选择器的单元测试

之前我们对应用程序代码进行了插桩(通过构建步骤或向Babel管道插入插件)。在上面的示例中,我们没有加载应用程序,而是仅运行测试文件本身。

如果我们想从单元测试中收集代码覆盖率,我们需要对_我们的规范文件_的源代码进行插桩。最简单的方法是使用相同的.babelrcbabel-plugin-istanbul,并告诉Cypress内置的打包器在打包规范时使用.babelrc。可以使用@cypress/code-coverage插件再次完成此操作,将以下代码添加到setupNodeEvents函数中。

const { defineConfig } = require('cypress')

module.exports = defineConfig({
// setupNodeEvents can be defined in either
// the e2e or component configuration
e2e: {
setupNodeEvents(on, config) {
require('@cypress/code-coverage/task')(on, config)
// 告诉Cypress使用.babelrc文件
// 并对规范文件进行插桩
// 只有额外的应用程序文件会被插桩
// 而不是规范文件本身
on('file:preprocessor', require('@cypress/code-coverage/use-babelrc'))

return config
},
},
})

作为参考,.babelrc文件在示例应用程序和规范文件之间共享,因此Cypress测试以与应用程序代码相同的方式进行转译。

{
"presets": ["@babel/preset-react"],
"plugins": ["transform-class-properties", "istanbul"]
}

当我们运行带有babel-plugin-istanbul的Cypress并检查规范iframe中的window.__coverage__对象时,我们应该看到应用程序源文件的覆盖率信息。

单元测试中的代码覆盖率

单元测试和端到端测试中的代码覆盖率信息格式相同;@cypress/code-coverage插件自动抓取两者并保存组合报告。因此我们可以在运行测试后看到cypress/e2e/selectors.cy.js文件的代码覆盖率。

选择器代码覆盖率

我们的单元测试覆盖了端到端测试无法到达的行,如果我们执行所有规范文件——我们将获得100%的代码覆盖率。

完整的代码覆盖率

全栈代码覆盖率

复杂的应用程序可能有一个具有自身复杂逻辑的Node后端。从前端Web应用程序,API调用经过多层代码。如果能跟踪在Cypress端到端测试期间执行了哪些后端代码,那就太好了。

我们的端到端测试在覆盖Web应用程序代码方面如此有效,它们是否也覆盖了后端服务器代码?

**长话短说:是的。**你可以从后端收集代码覆盖率,并让@cypress/code-coverage插件将其与前端的覆盖率合并,创建一个单一的全栈报告。

info

本节完整源代码可以在cypress-io/cypress-example-conduit-app仓库中找到。

你可以运行Node服务器并使用nyc动态对其进行插桩。代替"正常"的服务器启动命令,你可以运行package.json中定义的npm run start:coverage命令,如下所示:

{
"scripts": {
"start": "node server",
"start:coverage": "nyc --silent node server"
}
}

在你的服务器中,插入来自@cypress/code-coverage的另一个中间件。如果你使用Express服务器,包含middleware/express

const express = require('express')
const app = express()

require('@cypress/code-coverage/middleware/express')(app)

如果你的服务器使用hapi,包含middleware/hapi

if (global.__coverage__) {
require('@cypress/code-coverage/middleware/hapi')(server)
}

**提示:**你可以有条件地注册端点,只有在有全局代码覆盖率对象时才注册,并且你可以从覆盖率数字中排除中间件代码

/* istanbul ignore next */
if (global.__coverage__) {
require('@cypress/code-coverage/middleware/hapi')(server)
}

对于任何其他服务器类型,定义一个GET /__coverage__端点并返回global.__coverage__对象。

if (global.__coverage__) {
// 处理"GET __coverage__"请求
onRequest = (response) => {
response.sendJSON({ coverage: global.__coverage__ })
}
}

为了让@cypress/code-coverage插件知道它应该请求后端覆盖率,将新端点添加到Cypress配置环境设置中的env.codeCoverage.url键下。例如,如果应用程序后端运行在端口3000上,并且我们使用默认的"GET /coverage"端点,设置如下:

const { defineConfig } = require('cypress')

module.exports = defineConfig({
env: {
codeCoverage: {
url: 'http://localhost:3000/__coverage__',
},
},
})

从现在开始,端到端测试期间收集的前端代码覆盖率将与插桩后的后端代码覆盖率合并,并保存在一个报告中。以下是cypress-io/cypress-example-conduit-app示例的报告:

前后端代码的组合代码覆盖率报告

你可以在coveralls.io/github/cypress-io/cypress-example-conduit-app仪表板上探索上述组合的全栈覆盖率报告。你也可以在我们的RealWorld App中找到全栈代码覆盖率。

即使你只想测量后端代码覆盖率,Cypress也可以帮助你。

视频

我们录制了一系列视频展示Cypress中的代码覆盖率

如何为react-scripts Web应用程序进行代码覆盖率插桩

从Cypress测试获取代码覆盖率报告

从代码覆盖率报告中排除代码

使用第三方工具稳健检查代码覆盖率

向项目添加代码覆盖率徽章

在提交状态检查中显示代码覆盖率

检查拉取请求的代码覆盖率

示例

你可以在以下仓库中找到展示不同代码覆盖率设置的完整示例:

完整的示例列表链接在cypress-io/code-coverage#external-examples

另请参阅