最近咱们前端团队在重构大量的 UI 组件,为了保证代码质量,我要求团队中的成员必须编写单元测试,而且测试覆盖率达到 80% 以上。那么问题来了,为何是 80% 的覆盖率? 这是一个硬性的考核指标吗?javascript
这里所说的测试覆盖率,是指的是开发人员写的单元测试的覆盖率,不是测试人员的功能测试的覆盖率。html
为何须要写单元测试就再也不阐述,我相信你们都知道,特别是在持续集成过程当中的重要性。可是,从个人经从来看,当前的软件市场环境中,不论是用的瀑布模式,仍是螺旋模式,仍是敏捷模式,不少软件没有写单元测试。前端
我也是一个程序员,天天须要写一些的业务代码,对于写单元测试来讲,确实须要我不少时间和精力,由于它也须要设计用例和一些体力活。因此在咱们的一些项目中也存在不少功能没有单元测试,主要缘由有如下几个点:java
业务逻辑更新太快,单元测试不可复用;node
业务时间紧急,迭代周期时间短,没有时间写单元测试;git
UI 上不少测试,经过单元测试代码没法覆盖。程序员
在《软件测试》一书中讲测试的原则,第一条就是:“彻底测试程序是不可能的”。因此对于以上部分需不需测试,取决于你软件性质,时间和团队。可是对于知足如下几点代码我建议须要编写单元测试:github
和安全相关的代码逻辑;浏览器
核心的功能模块,函数;安全
短时间不会发生变化的 UI 组件;
提供外部调用的接口。
<!-- more -->
若是彻底经过测试覆盖做为质量标准是存在问题的,咱们在检查一个测试覆盖了的时候每每会经过一些工具去检查,程序员是能够经过一些方式让数字看上去漂亮,可是这没有意义。咱们应该把它做为一种发现未被测试覆盖的代码的手段,同时也是一种学习的手段,为何这段代码没有覆盖到? 若是这个函数的参数发生了变化会怎么样? 这段代码逻辑怎么这么复杂?
经过分析未被测试覆盖的代码,找到是设计问题,还功能理解有问题,仍是说着就是一段废代码,它能够帮助开发者可以更好的理解背后的事情,能够检查程序中的废代码,而后在之后的设计中作很好的抽象,作可测试的代码。
各类开发语言都有对应的测试框架,能够生成测试报告,在本文中我之前端的 javascript 为示例, karma
+ istanbul
工具生成报告。
karma
是一个测试框架;
istanbul
是 JavaScript 程序的代码覆盖率工具。
怎么生成测试报告这里就不讲,有不少教程,也能够查看官方文档 istanbul。这里咱们先来看一下生成出来的测试报告。 如下是 rsuite src/utils
目录下文件的测试报告, 这是打开的一个生成 html
格式的测试报告:
{% asset_img 1.png RSUITE 测试覆盖率 %}
从图中咱们能够看到它有四个指标:
Statements: 语句覆盖率,执行到每一个语句;
Branches: 分支覆盖率,执行到每一个if代码块;
Functions: 函数覆盖率,调用到程式中的每个函数;
Lines: 行覆盖率, 执行到程序中的每一行。
每个指标都列出了覆盖的比例和数量状况,其中
Statements
与Lines
比例和数量是一致的,那它们有什么不一样呢?
在代码中每每存在一些书写不规范的状况,好比一行多个语句,这个时候它们统计的覆盖率就会有差别。 这里又有一个值得思考的问题就是,代码覆盖率工具是怎么统计一行多个语句这种代码的? 后面讲到统计原理的时候会讲到。
另外,咱们经过图中能够看出 decorate.js
这个文件相对来讲测试覆盖率较低,咱们进入再具体分析一下,在那些地方没有覆盖到:
{% asset_img 2.png decorate.js 测试覆盖率 %}
从图中咱们能够看到红色部分和黄色, 都是在测试用例中没有覆盖到的地方:
getProps
函数,该函数式 export
出去的一个函数,可是在测试用例中没有覆盖到;
typeof size === 'object'
代码块没有覆盖到;
Component.propTypes={}
.. 这里黄色部分,是一个默认值设置,说明这个默认值一直没有被使用过;
在图中左侧,显示行号的地方有一个 12x
、9x
、4x
,这个表明了该行语句被执行的次数, 经过这个清晰的报告,咱们能够在代码中看出那些函数,那些代码块没有被执行,从而去分析缘由,修正测试用例,完善代码逻辑,提升质量。
我先来看一下 istanbul
生成的测试报告中有个 lcov.info
文件, 这里我只贴出关于 decorate.js
文件这部分的内容:
SF:/Users/simonguo/workspace/rsuite/src/utils/decorate.js FN:25,getClassNames FN:39,getProps FN:41,(anonymous_2) FN:50,decorate FN:51,(anonymous_4) FNF:5 FNH:3 FNDA:237,getClassNames FNDA:0,getProps FNDA:0,(anonymous_2) FNDA:12,decorate FNDA:12,(anonymous_4) DA:4,1 DA:11,1 DA:18,1 DA:27,237 DA:28,237 DA:30,237 DA:32,237 DA:40,0 DA:41,0 DA:42,0 DA:44,0 DA:51,12 DA:52,12 DA:53,12 DA:54,12 DA:56,12 ...
FN
表明函数,25
,39
,41
,50
,51
这些行分布对应源代码中的函数开始的行号,FNF:5
表明一共有5个函数FNH:3
其实 3 个函数被测试所覆盖,FNDA:237,getClassNames
表明了 getClassNames
这个函数被执行了 237 次。
...
等等,在文件中详细记载了行号,以及代码的执行状况,你们能够再对照前面的那张“测试覆盖率”图片进行分析,能够详细的看出整个 lcov.info
文件中记录内容。有了这样一份记录信息就可以生成出一份可视化的测试报告,也能够上传到 coveralls,展现给你们。 那么这里须要思考的问题是,这样一份数据统计记录是怎么统计出来的呢?
若是但愿有些代码被忽略,不进入覆盖统计,istanbul 提供注释语法 ,查看Ignoring code for coverage purposes
javascript 覆盖率统计的核心思想,是在源代码相应的位置注入设定的统计代码,当执行测试代码的时候,代码运行到注入的地方,就会执行对应的统计代码,生成覆盖率统计报告。大概步骤以下:
第一步:生成语法树,对源代码进行语法分析,解析,而后生成语法树。
生成出来的结构以下,这段代码来自
esprima
, A simple example on Node.js REPL:
> var esprima = require('esprima'); > var program = 'const answer = 42'; > esprima.tokenize(program); [ { type: 'Keyword', value: 'const' }, { type: 'Identifier', value: 'answer' }, { type: 'Punctuator', value: '=' }, { type: 'Numeric', value: '42' } ] > esprima.parse(program); { type: 'Program', body: [ { type: 'VariableDeclaration', declarations: [Object], kind: 'const' } ], sourceType: 'script' }
第二步:注入统计代码,在语法树相应的位置注入统计代码,在程序执行到这个位置的时候对相应的全局变量赋值,确保执行以后可以根据全局变量知道代码的执行流程。到这里就解决了前面说的“一行若是有多个语句怎么统计?”的问题。
第三步:再把注入统计代码的语法树,生成对应的 javascript 代码。
如下是
escodegen
的一段示例代码
// A simple example: the program escodegen.generate({ type: 'BinaryExpression', operator: '+', left: { type: 'Literal', value: 40 }, right: { type: 'Literal', value: 2 } }); // produces the string '40 + 2'.
第四步:将生成好的 javascript 代码交给执行环境(nodejs或者浏览器)运行。
第五步:执行单元测试,产生的统计信息,放到全局标量中。
第六步:根据全局标量中的覆盖率信息生成特定格式的报告,这样咱们就看到了 lcov.info
文件和 .html
文件。
这个步骤是依据 istanbul
统计 javasript 的原理,其余语言的一些统计工具没有接触过,可是基本的思想应该都是大同小异的。在 javasript 对语法分析,生产语法树再还原 javasript 代码是有一些开源工具的,因此若是有兴趣的童鞋要本身实现一套代码覆盖率的功能,只须要写好注入的统计代码逻辑和运行环境的处理。
对一个持续集成的项目来讲,单元测试很是重要,同时最好具备较高的测试覆盖率。再次强调测试覆盖率是一种发现未被测试覆盖的代码的手段,它不是一个考核质量的目标。
另外,咱们维护的开源项目 rsuite ,是一套 React 的 UI 组件库,若是你对此感兴趣,或者使用中遇到任何问题,能够联系咱们 Discord: join chat
本文做者:郭小铭