软件构建(十)——测试与重构

测试可以由开发人员或专门的测试人员进行。按层级分,测试可以分为单元测试、组件测试、继承测试、回归测试、系统测试;按是否了解对象内部工作机制分,测试可以分为黑盒测试和白盒测试。本文关注的是开发人员所进行的白盒测试。重构是“在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改”的过程。本文将讲述软件演化以及重构的一些理念。

测试和重构都是非常大的话题,大到足以各用一本经典著作来阐述,如《软件测试》《重构:改善既有代码的设计》。因此本文也仅是记录一些要点,如果要深入了解,还是要阅读相关的书籍为好。

1. 开发者测试

1.1 开发者测试在软件质量中的角色

  • 测试的目标是找出错误,与其他开发活动背道而驰
  • 测试永远不可能彻底证明程序中没有错误
  • 测试本身并不能改善软件的质量,只能用来指示
  • 测试时要求你假设会在代码里面找到错误

根据项目大小和复杂程度的不同,开发者测试应该占整个项目时间的8%~25%。测试得出的结果,可以用来评估产品的可靠性,并指导对软件的修正。

1.2 推荐方法

  • 对每一项相关的需求进行测试,以确保需求都已经被实现:最好在需求阶段就计划好这一部分的测试用例,并注意测试安全级别、数据存储、安装过程及系统可靠性等这些厂被忽略的测试点
  • 对每一项相关的设计关注点进行测试,以确保设计已经被实现
  • 使用一个检查表,其中记录着你在本项目迄今为止所犯的,以及在过去的项目中所犯的错误类型
  • 推荐先写测试用例再写代码:可以更早地把需求问题暴露出来,迫使你写代码前思考一下设计,并更早地发现代码中的缺陷

1.2.1 结构化基础测试

一个子程序所需的测试用例的最少数量可以用下面的简单方法计算:

  1. 对通过子程序的直路,开始的时候记1
  2. 遇到if、while、repeat、for、and以及or关键字或者其等价物时,加1
  3. 遇到每一个case语句就加1,如果case语句没有缺省情况,则再加1

假设一个子程序有5个if或for语句,那么至少需要6个测试用例,其中1个测试所有布尔条件都为真的正常情况,其余5个测试每一种假的情况。不过,结构化基础测试能够向你保证所有的代码都得到执行,但它并不能说明数据的变化情况。

1.2.2 数据流测试

编写数据流测试用例的关键是要对所有可能的定义-使用路径进行测试,即对每一个变量测试所有在某处定义而在另一处使用的组合。因此添加完整的“己定义-已使用”所需的用例可以覆盖到结构化基础测试所覆盖不到的情况。

1.2.3 其他测试建议

  • 猜测错误:猜测程序会在哪里出错的基础之上建立测试用例,通常基于直觉或者过去的经验
  • 边界值分析:除了分析边界点、允许的最大最小值之外,还应该注意多个变量互相关联时的边界
  • 测试几类坏数据:数据太少(没有数据),数据太多,无效数据,长度错误的数据,未初始化的数据
  • 测试几类好数据:正常的情形(所期望的值),最小和最大的正常情况,与旧数据的兼容性
  • 采用容易手工检查的测试用例

1.3 改善测试过程

1.3.1 测试支持工具

  • 为测试各个类构造脚手架:使用“模仿对象(mock object)”或“桩函数(stub routine)”来模拟测试
  • Diff工具:将程序输出重定向到一个文件,并与预计输出文件作比较
  • 测试数据生成器:正确设计的随机数据生成器可以产生不寻常的测试数据组合,并且比手工构造数据更能彻底对程序进行测试
  • 覆盖率监视器:跟踪哪些代码己经测试过了,而哪些代码还没有
  • 数据记录器/日志记录器:监视程序,并在错误发生的时候为收集程序状态信息;另外可考虑编写自己的数据记录工具,并编译进开发版本中
  • 符号调试器:对代码进行单步调试,跟踪变量的值等等调试手段,可以作为测试和走查代码的辅助手段
  • 系统干扰器:这类工具有内存填充、内存抖动、选择性内存失败(模拟内存不足的情况)、内存访问边界检查(监视指针操作)等系统功能
  • 错误数据库:用于存放以往的错误,以便检查重复出现的错误

1.3.2 改善测试过程

  • 有计划的测试:就重要性而言,测试应该于设计和编码平起平坐,这就要求项目重视测试并保障这一过程的质量
  • 回归测试:要保证每次回归测试都使用相同的测试用例,随着产品的不断发展,会添加新的测试用例,但仍应保留旧的用例
  • 自动化测试:管理回归测试唯一可行的方法,就是将其变成一个自动化的过程

1.3.3 保留测试记录

为了确定所做的修改对整个项目的影响,通常可以收集这些数据以供参考:缺陷的管理方面描述(报告日期、人员、描述、修正错误日期等等),问题的完整描述,复现错误所需要的步骤,绕过该问题的建议,相关的缺陷,问题的严重程度,缺陷根源(需求、设计、编码还是测试),对编码缺陷的分类,修正错误所改变的类和子程序,缺陷所影响的代码行数,查找该错误所花的小时数,修正错误所花费的小时数等等。

2. 重构

2.1 软件的演化与重构

软件演化就像生物进化一样,有些突变对物种是有益的,另外一些则是有害的。区分软件演化类型的关键,就是程序的质量在这一过程中是提高了还是降低了;第二个标准是演化是源于程序构建过程还是维护过程中的修改,毕竟构建时由最初开发人员完成,没有什么修正压力,而维护时的修改则需要面对已发布产品和用户的压力。

软件演化是无法避免且具有重要意义的现象。当你有机会或迫不得已需要对代码进行改变时,就努力对代码进行改进(重构),这样未来在开发中调整就会更容易。

重构的理由有许多,如代码重复,冗长的子程序,过长或嵌套过深的循环,内聚性太差的类等等。无论是哪种情况,代码都会有一些警告信号,这就是所谓的代码的“坏味道”。关于重构的更详尽的理由在专门讲重构的书都列得非常清楚,此处不再赘述。

2.2 各层级的重构

  • 数据级
    • 用具名常量替代神秘数值
    • 使变量的名字更为清晰且传递更多信息
    • 用函数来代替表达式
    • 用多个单一用途变量代替某个多用途变量
    • 将一组类型码转化为类或枚举类型
  • 语句级
    • 将复杂布尔表达式转换成命名准确的布尔函数
    • 合并条件语句不同部分中的重复代码片段
    • 使用break或return而不是循环控制变量
    • 在嵌套的if-else语句中一旦知道答案就立即返回,而不是去赋一个返回值
    • 用多态来替代条件语句(尤其是重复的case语句)
  • 子程序级
    • 将冗长的子程序转换为类
    • 将查询操作从修改操作中独立出来
    • 合并相似的子程序,通过参数区分它们的功能
    • 简化或去除无用的参数
  • 类实现
    • 如果一组派生类的差别仅仅是虚函数返回的常量不同,应用数据初始化替代虚函数
    • 整理成员函数或成员数据的位置
    • 将特殊代码提取为派生类,将相似的代码结合起来放置到基类中
  • 类接口
    • 将包含多个不同功能的类进行拆分
    • 删除无所事事的类
    • 去除多余的中间者调用
    • 对暴露在外的成员变量进行封装
    • 对于不能修改的类成员,删除相关的Set()成员函数
  • 系统级
    • 为无法控制的数据创建明确的索引源:例如GUI控件中维护的数据无法方便或一致地访问,可以创建一个类来映射其中的数据,并将此类作为该数据的明确来源
    • 基于类型码创建对象时,用工厂模式而不是简单地实例化对象

2.3 安全重构的方法

  • 在开始重构之前,保存初始代码
  • 缩小重构的步伐,不要一次性大动干戈
  • 同一时间只做一项重构
  • 把要做的事情列出来,并记录下在重构过程中发现的需要进行的另外一项重构任务
  • 增加测试用例,重构完后重新测试
  • 根据重构风险级别来调整重构方法:如果是高风险的重构,务必做好测试工作,请其他人来检查重构工作甚至采用结对编程

重构是一剂良药,但也有被滥用的可能性,因此不要把重构当做先写后改的代名词,也要避免用重构代替重写

关于重构的时机,如果拿捏不准,可以考虑这些建议:在增加子程序和类,在修补缺陷的时候进行重构;关注易于出错和高度复杂的模块;在维护环境下改善你手中正在处理的代码;定义清楚干净代码和拙劣代码之间的边界,然后尝试把代码一部分一部分移到理想的一边。

参考文献:电子工业出版社《代码大全(第2版)》第22、24章

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器