第三章 代码检查、走查与评审
编写的代码一开始时,只是给机器执行的,并没有想着供人们阅读,到了20世纪的70年代,一些程序员才开始意识到阅读代码对于完善软件测试和调试的价值。到了今天,并不是所有的软件测试人员都要阅读代码,但是去研读程序的代码也是测试工作的一部分,已经得到了广泛的认同了。
3.1 影响到特定的测试和调试工作需要人工实际阅读代码可能性的几个因素:
- 软件测试规模和复杂度
- 软件开发团队的规模
- 软件开发的时限(例如时间安排表是松散还是紧张)
- 编程小组的技术背景和文化等
3.2 人工测试
- 人工测试技术在查找错误方面非常有效,以至于任何编程的项目都应该使用其中的一种或是多种的技术来测试。在程序开始编码前前期,中期,后期都可来进行设计测试。
- 重要的注意事项:由于包含了人为的因素在内,导致很多方法的正规性要差于由计算机执行的数学证明,我们可能会去怀疑有些简单和不正规的东西是不是有用。反之,这些不正规的方法并没有影响到测试取得成功;相反,它们还从以下两个方面显著的提高了测试的功效和可靠性:
- 我们通常认为错误发现的越是早,改错误的成本会越低,正确的改正错误的可能性也越大
- 程序员在开始基于计算机的测试时似乎会经历心理上一个转变。从程序人员自身的内部压力似乎会越来越大,产生一个想法,要:“尽可能的修复这个缺陷”。由于这些压力大的存在,程序员在改正某个Bug时,要比改正早期发现的问题时所犯的失误会更多一些。
3.3 检查与走查
代码检查和走查都是人工检测的方法。这种测试技术在编码之后计算机测试之前使用,要求人们组成一个小组来阅读和检查程序,可以有效的在项目早期发现错误,并改正错误。代码检查和代码走查有以下的相同点:
- 组成小组来阅读或直观检查特定的程序
- 在代码走查中,一组开发人员(3-4人最佳)对代码进行审核。参加者当中只有一人是程序编写者。
- 代码检查与走查是对过去桌面的检查过程(在提交测试前程序员阅读自己的程序的过程)的改进。
- 代码的优点在于,一旦发现错误,通常就能在代码中对其精确的定位,这就降低了调试(错误修正)的成本。
- 这些方法通常可以有效的查找出30%~70%的逻辑设计和编码错误。但是,这些方法不能有效的查找出高层次的设计错误,例如:在软件需求分析阶段的错误。请注意,所说的30%~70%的错误发现率,并不说所有的错误中多达70%可能会被找出来,而是讲这些方法在测试的过程结束时,可以有效的查找出多达70%的已知错误。请记住:程序中的错误总数始终是未知的。
- 人工方法只能发现“简单”的错误,而困难的、不明显的或是微妙的错误只能用基于计算机的测试方法才能找到。代码检查、走查基于计算机的测试是互补的。缺少任何一种,错误检查的效率都会降低。
- 修改一个现存的程序比编写一个新程序更容易产生错误(以每一行代码的错误数量计算)。因此,除了回归测试方法外,更改后的程序还要进行人工的这些方法。
3.3.1 代码检查
代码检查就是以组为单位来阅读代码,是一系列规程和错误检查技术的集合。代码检查的大多数讨论都集中在规程、所要填写的表格等。
一个代小组通常有4个人组成的,其中一个人发挥着协调的作用。协调人应该是称职的程序员,不能是该程序的编写人员,无需对程序细节了解的很清楚。
协调人的职责:
- 为代码检查分化材料、安排进程
- 在代码检查中起主导作用
- 记录发现所有的错误
- 确保所有的错误随后得到改正
——协调人就像质量控制的工程师。
(1)代码检查小组
- 通常包括四个人:协调人、代码作者、其他程序设计人员、测试专家。
- 协调人职责:1.分发材料、安排进程;2.记录发现的错误;3.确保错误随后的改正。
- 代码作者职责:逐条讲解程序代码的逻辑结构。
- 其他程序设计人员:提问题,并判断程序是否存在错误。
- 测试专家:熟悉软件测试,并知道大部分的常见编码错误。
(2)检查议程与注意事项
- 在代码检查的前几天,协调人将程序清单和设计规范分发给其他的成员
- 代码在评审时:a、由程序人员逐条语句讲解程序的逻辑结构; b、对着历来常见的编码错误列表分析程序。
- 协调人负责确保检查会议的讨论高效的进行
(3)代码评审后
程序员会得到一份已发现错误的清单。这份错误清单应进行分析、归纳,用以提炼错误列表,以便提高以后代码检查的效率。
(4)代码检查的时间以及地点
- 避免所有的外部干扰
- 会议时间理想的时间应在90~120分钟
(5)代码检查时需注意
程序员得是非自我的态度对待检查过程,改进软件的质量。不应将代码视为对其人格的攻击,检查将会是无效的。
3.4 用于代码检查的错误列表
代码检查过程重要的一个部分就是对照一份错误的列表,来检查程序是否存在常见错误。然而,有些错误列表更多的注重编程的风格而不是错误。
3.4.1 数据引用错误
1、是否引用的变量未赋值或未初始化?引用每个数据项时,应试图非正式的 “证明” 该数据项在当前的位置具有确定的值。 |
2、对于所有的数据引用,是否每一个下标的值都在相应规定界限内。 |
3、对于所有的数组的引用,是否每一个下标的值都是整数?虽然在某些语言中这不是错误,但这样做是危险的。 |
4、是否是“虚调用”,应试图非正式的证明,对于使用指针值得引用,引用的内存单元者都存在。 |
5、内存区域有不同属性的别名,通过别名引用时,内存区域中的数据值是否具有正确的属性? |
6、变量值得类型或属性是否与编译器所预期的一致。 |
7、当内存分配的单元小于内存可寻址的单元大小时,是否存在直接或是间接的寻址错误? |
8、当使用指针或是引用变量时,被引用的内存属性是否与编译器所预期的一致? |
9、数据结构在多个过程或是子程序中被引用,那么每个过程或子程序对该结构的定义是否都相同。 |
10、字符串的索引,对数组进行索引操作或下标引用时,字符串的边界取值,是否有 “仅差一个” 的错误 |
11、对于面向对象语言,是否所有的继承需求都在实现类中得到了满足? |
3.4.2 数据声明错误
1、是否所有的变量都进行了明确的声明?没有明确声明虽然不一定是错误,但通常却是麻烦的源头。 |
2、如果变量所有的属性在声明中没有明确说明,那么默认的属性能否被正确理解? |
3、如果变量在声明语句中被初始化,那么它的初始化是否正确? |
4、是否每个变量都被赋予了正确的长度和数据类型? |
5、变量的初始化是否与其存储空间的类型一致? |
6、是否存在着相似名称的变量?(名称相似程序中可能会被混淆)。 |
3.4.3 运算错误
1、是否存在不一致的数据类型(如非算术类型)的变量的运算? |
2、是否有混合模式的运算? |
3、是否有相同数据类型不同字长变量间的运算? |
4、赋值语句的目标变量的数据类型是否小于右边表达式的数据结果类型或是结果? |
5、在表达式运算中间的结果是否存在表达式向上或向下溢出的情况。 |
6、除法运算中的除数是否可能为0? |
7、如果计算机表达变量的基本方式是基于二进制的,那么运算结果是否不精确? |
8、在特定场合,变量的值是否超出了有意义的范围? |
9、对于包含一个以上操作符的表达式,赋值顺序和操作符的优先顺序是否正确? |
10、整数的运算是否有使用不当的情况,尤其是除法? |
3.4.4 比较错误
1、如果程序包含多条分支路径,比如有计算GOTO语句,索引变量的值是否会大于可能的分支数量? |
2、是否所有的循环最终都终止了?应设计一个非正式的证据或论据来证明每一个循环都会终止。 |
3、程序、模块或子程序是否最终都终止了? |
4、由于实际情况没有满足循环的入口条件,循环体是否有可能从未执行过?发生了,是不是存在一处疏漏? |
5、如果循环同时由迭代变量和一个布尔条件所控制,如果循环越界了,后果会如何? |
6、是否存在“仅差一个”的错误,如迭代数量恰恰多一次或少一次?这在从0开始的循环中常见。 |
7、如果编程语言中有语句组或代码块的概念,是否每一组语句都有一个明确的while语句,并且do语句也与其相应的语句组对应?或是每一个左括号都对应有一个右括号? |
8、是否存在不能穷尽的判断? |