11月阅读周·编写可测试的JavaScript代码:代码覆盖率之代码覆盖率数据篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》。
当前阅读周书籍:《编写可测试的JavaScript代码》。
代码覆盖率数据
代码覆盖率数据通常分为两部分,代码行的覆盖率和函数的覆盖率,这两种覆盖率最容易用百分比表示。对于单个单元测试来说,这些数字很容易理解。在测试对象中的单个函数或方法时,该文件中的所有函数数量或代码行数会作为分母进行百分比的计算。所以,对单个模块,要测试多个文件的话,每个测试的单元测试覆盖率就会降低。将所有测试的覆盖率数字聚合在一起,就会为该文件形成完整的覆盖率信息。
同样,在尝试确定整个应用程序(或一个目录下的多个文件)的总覆盖率时,默认情况下,总覆盖率将不包括任何没被测试的文件。覆盖率度量是完全忽视没有被测试过的任何文件的,所以要注意“最终”的聚合数字。
要整体了解代码覆盖率,必须要生成一些“假的”或“空的”测试文件,用于模拟没有被测试过、也因此没有任何覆盖率的文件。
接下来,让我们一起使用YUI的覆盖率工具做覆盖率的演示。最新版本解压缩后,我们会看到java/build目录包含了两个必要的JAR文件:yuitest-coverage.jar和yuitest-coverage-report.jar。当然,我们需要安装一个近期版本(1.5或更高版本)的Java运行时环境,并设置环境变量。
第一个JAR用于将JavaScript文件转换为代码覆盖率所用的instrumented文件,而第二个JAR文件则用于将抽取出的覆盖率信息生成报告。
转换instrumented文件
将普通JavaScript文件转换为instrumented文件是非常简单的:
java -jar yuitest-stopCoverage.jar -o coveraged.js instrument-me.js
coveraged.js文件是instrument-me.js 文件的完整工作副本,并带有嵌入式覆盖率跟踪。如果阅读该文件,会看到它是如何被转换的:在每个语句中间都有一个新语句,用于对全局变量增加计数器,以跟踪该行代码是否被执行过。该instrumented文件是原始文件的替代。执行完这个代码后,可以从全局变量_yuitest_coverage中提取出该数字。
要从单元测试中获取代码覆盖率信息,只需要简单将所测试代码生成相应的instrumented文件,并执行这些测试,然后提取覆盖率信息。
要从集成测试中提取代码覆盖率信息,需要将应用程序的所有JavaScript文件都转换成instrumented文件,并像正常代码一样部署这些文件,对构建的instrumented文件运行Selenium测试(或手工测试),然后提取覆盖率信息。通常,可以使用一个单独的find操作符将所有的代码一次性转换成instrumented文件(当然,下面的代码也都要在一行执行才行):
find build_dir -name "*.js" -exec echo "coveraging {}" \;
-exec java -jar yuitest-coverage.jar -o /tmp/o {} \;
-exec mv /tmp/o {} \;
这里,我们从部署的根目录开始,查找所有的JavaScript文件,使用覆盖率版本的文件替代它们。然后照常打包并部署应用程序。然而,处理代码覆盖率是按行而不是按语句处理的,所以要确保在非压缩JavaScript文件上运行覆盖率工具。
coveraged文件剖析
YUI覆盖率工具需要将纯JavaScript文件转换成一个可以追踪哪些行代码或函数已经被执行的中间文件(译者注:instrumented文件,下文也称coveraged版本的文件)。要了解该行为,让我们重温一下sum函数:
function sum(a, b) {
return a + b;
}
将该文件转换成coveraged版本:
java -jar yuitest_coverage sum.js -o sum_cover.js
如果还没定义全局变量和函数,以便让YUI Test Coverage追踪其信息的话。剩余的代码如下:
_yuitest_coverage['sum.js'] = {
lines: {},
functions: {},
coveredlines: 0,
calledlines: 0,
coveredfunctions: 0,
calledfunctions: 0,
path: '/home/trostler/sum.js',
code: [],
};
_yuitest_coverage['sum.js'].code = ['function sum(a, b) {', 'return a + b;', '}'];
_yuitest_coverage['sum.js'].lines = { 1: 0, 2: 0 };
_yuitest_coverage['sum.js'].functions = { 'sum:1': 0 };
_yuitest_coverage['sum.js'].coveredLines = 2;
_yuitest_coverage['sum.js'].coveredFunctions = 1;
_yuitest_coverline('sum.js', 1);
function sum(a, b) {
_yuitest_coverfunc('sum.js', 'sum', 1);
_yuitest_coverline('sum.js', 2);
return a + b;
}
第一块代码定义了_yuitest_coverage["sum.js"]对象,该对象用于记录代码行数和函数数量,以及其他一些用于记录信息的属性。
继续设置最后一块代码,其定义了行数。
覆盖率行数的统计是发生在每个_yuitest_coverline和_yuitest_coverfunc的调用时。这些函数是在样板代码中定义的,但是它们所做的也就是增加函数或者代码行数的数量。
因此,本例中,文件加载后,就会执行_yuitest_coverline("sum.js", 1),每个文件的第一行都会被执行。实际执行sum的时候,会先执行_yuitest_coverfunc,也就是告知大家sum函数已经执行了,然后再执行_yuitest_coverline,以递增已执行代码的行数,最后再执行实际的return a+b,然后就结束了。
通过这种方式,在每个文件的开头、每个语句之间、每个函数开头,YUI Test Coverage都会跟踪执行过的代码行和函数。
总结
重要的是要注意,每次加载该文件的时候,计数都会复位。可以使用缓存预防这种情况,但请注意,如果一个文件被加载多次,之前统计的覆盖率信息都将会丢失。最好是在每个测试运行结束后,就提取覆盖率结果。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)