11月阅读周·编写可测试的JavaScript代码:单元测试之编写好的单元测试篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读十个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》、《JavaScript异步编程设计快速响应的网络应用》。
当前阅读周书籍:《编写可测试的JavaScript代码》。
编写好的单元测试
怎样编写一个好的单元测试?代码覆盖率,不管喜欢与否,都是等式的一部分。等式的另外一部分是参数的边界测试和非边界测试。考虑到函数的依赖已经模拟了(单元测试本身是智能的),参数和外部对象的桩模拟是单元测试人员唯一且必须用到的突破口。举个例子,使用我们的老朋友sum函数,该函数接受两个参数,每个参数都可以有六种类型(数字、字符串、对象、undefined、null或布尔值)——所以,完美的单元测试将会测试所有这些类型的组合。但这样的测试又将测试什么呢?比如,测试将null和undefined相加又有什么意义?
带着这些问题思考,或许一个好的单元测试的两个最重要因素是隔离和作用域。在下面章节,我们将看到隔离、作用域与测试是密切相关的。
隔离
单元测试应该只加载所需测试的最小化代码进行测试。任何额外的代码都可能会影响测试或被测试代码,而且还会产生问题。通常情况下,单元测试只有一个方法。这种方法可能是包含一个类或模块的文件中的一部分。必须加载所对应代码的物理文件。可惜的是,被测试的方法既有可能依赖于当前文件的其他函数或方法,也有可能依赖于外部依赖(如其他文件中的依赖)。
为了避免加载外部依赖,我们可以使用模(mock)、桩(stub)以及测试替身(test double)。稍后我将提供这些策略的详细信息,但现在重要的是要注意,它们都试图尽可能地将被测试代码与其他代码进行隔离。
单元测试测试的是代码的最小部分——方法,而不是其他。要达到此目的,测试代码必须尽可能地隔离所测试的方法。
范围
单元测试的范围——实际要测试的东西——必须很小。一个完全隔离的方法可以让测试的范围尽可能地小。良好的单元测试只会测试一种方法,它们不应该依赖于被调用或使用的其他方法。最底层的单元测试,断言应该只依赖于被测试的方法以及能够让该方法工作的任意mock、stub和double。
单元测试的成本很低,应该多做单元测试。就像一个“普通”的方法不应该去尝试做多件事情一样,单元测试也是同样的道理。尽量测试并加载最小量的代码,以确保能够成功执行应用程序的单元测试。单元测试通常关注于单一函数或方法;然而,需要由我们自己定义能够适应测试的一个“单元”的大小。
定义函数
在探索更进一步的细节之前,我们需要回过头来看一下。除非我们知道正在测试什么,以及结果应该是什么,否则编写良好的单元测试是不可能的。预期结果应该如何定义?测试如何定义?通过阅读和理解测试(即便已经存在了)来辨析一个函数应该如何工作,并不是理想的方法。注释定义了函数应该返回的结果。如果注释不保存实时更新,这种方式可能也不是最理想的。所以,是不是单元测试和注释这两个地方需要实时更新?是的,就应该这样。在编码之前,利用测试驱动开发(TDD)先编写单元测试,并不能避免函数所需要的注释。
注释
sum函数完全没有注释,所以在此,我们不知道该函数要做什么。字符串相加?还是分数相加?null和undefined呢?对象相加吗?函数必须告诉我们才行。利用Javadoc语法添加注释后的示例如下所示:
/* This function adds two numbers, otherwise return null
* @param a first number to add
* @param b second number to add
* @return the numerical sum or null
*/
function sum(a, b) {
return a + b;
}
现在可以测试这个函数了,因为我们已经知道要测试的内容了:数字。对不明白其意的函数,是不可能编写一个好的单元测试的。不能凭空编写一个好的单元测试。编写良好单元测试的第一规则是有一个完全规范定义的函数。
该函数实际实现的功能是否能如其所说?这就是单元测试要判断的——判断函数是否符合其预期行为。在这方面,第一步是定义函数的“正确”行为。注释是至关重要的,注释的缺陷对测试人员和维护人员来说都是非常危险的。很遗憾,没有一个测试能够自动确定注释是否存在缺陷,但是对于代码覆盖率,使用像jsmeter这样的工具,至少会提醒我们代码中的函数没有任何注释或注释太少。除了代码大小,检验代码bug的第二个最重要的指标是缺失或错误的注释,所以不但要有注释,而且确保注释准确也是非常重要的。
理想情况下,通过阅读函数的注释,我们(或其他人)就应该能够编写单元测试。这些注释可以通过JSDoc或YUIDoc、Rocco进行文档生成,以便让注释更加有用。同样重要的是,函数定义要保持最新。如果参数或返回值改变了,确保注释也要被更新。
作为TDD的拥护者,在编码之前编写测试,可能会减轻一些编写注释的负担。可以利用测试函数作为注释,因为测试(不像注释)必须保持最新;否则,它们就会执行失败。即使在没有任何代码之前就编写测试,带有足够注释的函数头也仍然能让未来的维护人员一眼就了解函数的功能是什么。进一步说,注释文档的发布有利于代码重用,使其他开发人员能够更容易找到代码,并且使用里面的函数,而不用再自己编写。对于公共方法,其他开发人员需要一个容易浏览的界面,以确保其可重用性。
测试用例
如果先编写测试用例,也可以用于规范函数(或被测试代码)功能。因为测试用例必须要进行维护以确保与代码保持同步(否则,测试就会失败!),测试用例最终决定代码,而不是注释。从理论上讲,注释可能会不同步更新,但测试不会,因此测试比注释更值得信赖。我认为大家不会陷入这个误区:作为标准开发实践的一部分,要保持注释实时更新。维护我们代码的下一个人,通过阅读注释就可以很容易明白意思,而不用转到另外一个文件来查阅测试代码。
正向测试
正向测试应该是首先要编写的单元测试,因为在构建负向测试和边界测试之前,它们提供了基本的预期功能。在这里,需要测试的是所有常见的用例、函数是如何调用并使用的。
负向测试
有些Bug发生时,很难找到发生Bug的地方,负向测试可以帮助找到这些代码。代码是否可以处理非期待值?在测试中,传入非期望的函数或该函数不想要的参数进行测试,确保被测试的函数能够正确处理它们。
负向测试通常不会和正向测试找到相同数量的bug,但负向测试发现的bug通常是难以对付的bug。在一般时候这些难以发现的错误是不会被发现的。一般在边角测试或非预期情况发生时才能发现。在这种情况下,该函数做了什么?除了履行契约定义,是否能够理智面对非期望输入或状态?
代码覆盖率
代码覆盖率是一种度量方法,通常是指执行代码与非执行代码行数之间的百分比。它通常用于展示,对于特定测试,有多少代码被测试到了。可以编写更多的测试来“覆盖”没有测试到的代码,通常是以一定的比例来编写。我们将在第5章深入讨论代码覆盖率,但是现在,我只想说,代码覆盖率是有效单元测试的另一个关键部分。代码覆盖率百分比很高时,会产生误导,代码覆盖率较低时,更是如此。代码覆盖率不到50%的单元测试是红色标记,60%到80%代码覆盖率的单元测试是最有效的,任何超过80%代码覆盖率的单元测试都是大发横财。试图让单元测试覆盖保持在80%以上,会产生收益递减规律。记住,单元测试并不是质量保证(QA)的唯一之路,但这是为数不多的开发人员所负责的质量指标。在第5章,我们将了解如何测量和可视化代码覆盖率。
总结
重要的是,代码覆盖信息是自动生成的,无任何人工干预。一旦人工干预,情况就会迅速恶化。代码覆盖率的生成过程必须是无缝的。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)