软件质量保障
所寫即所思|一个阿里质量人对测试的所感所悟。
1. 介绍
有句话说:证实容易,证伪难。正如测试一样,证明缺陷存在容易,但证明不存在缺陷难。而变异测试颠覆了这一原则,如果我们知道存在缺陷,那么我们的测试结果会如何反映测试的质量呢?
随着工程师越来越多地采用更自动化的软件验证方法,以及在不断缩短的发布周期中对更高品质的软件输出的需求日益增长,变异测试帮助我们退一步评估,我们是否真的应该对我们的测试充满如此信心。
Mutation testing is the process of heuristically determining semantics of your program that are not covered by tests.
变异测试是通过启发式方法确定程序中未被测试覆盖的语义的过程。——Markus Schirp
在多数软件测试方法中,很难预判能否在测试过程中发现缺陷,往往直到这些缺陷在后续的测试环节被发现,甚至是更糟的情况下,在生产环境中出现时才会被注意到。这对于每位测试经理来说都颇为熟悉,因为他们需要根据生产环境发布后获得的经验来反馈并改进测试流程。无疑,是否存在一种更佳的方法来揭示测试覆盖中的问题呢?
变异测试作为一种测试技术,其历史可追溯至1971年,近年来越来越受到重视,现已发展出十几种相关测试工具,并广泛应用于各种软件环境。可用工具的数量自1981年的不足5种显著增长,到2013年已超过40种。
变异测试处在依赖系统期望行为正式规范的测试技术灰色地带,与之并列的还有模糊测试、变形测试以及探索性测试。从根本上讲,变异测试是一种实践,即对软件(所谓的“变异体”)的多个版本执行一组测试。每个待测软件版本都有意且通过编程方式注入了不同的缺陷。每一次测试迭代都是基于启发式方法在软件的一个微小差异版本上进行的——这些规则对应于常见缺陷——注入不同的缺陷。这些版本被称为“变异体”,意指它们是小规模的变异。测试的目的通常是确定哪些缺陷能被测试程序发现,从而导致执行失败。
之所以将软件的这些人造缺陷版本称为“变异体”,是因为它们的变异方式类似于人类基因在自然进化过程中的变异,这与人工智能中使用遗传算法解决搜索和优化问题有相似之处。
变异测试的好处可能非常明显,它能为以下方面提供极为有用的洞察:
• 自动化测试的质量和覆盖范围,尤其是断言和验证的覆盖充分度情况
• 特定测试设计技术的成功实施
• 代码不同模块的复杂度和可维护性
• 通过测试追溯组件到整体业务功能的能力
它可以作为一种方法来改进现有测试集,以确保有足够的测试验证或优先针对代码的特定模块进行测试。最终,它提供了一组关于质量的真实信息,这些信息通过其他方式是无法获得的。
2. 自动化测试
从概念上讲,变异测试并不局限于自动化测试;从原则上讲,它是一种可以应用于手工测试的方法。然而,在大多数情况下与运行数千次手工测试相关的成本无法与收益相提并论。
自动化测试的核心挑战之一是确保断言和验证点的同步。测试工程师理应感知更多的上下文知识,并利用专家经验来确定测试用例的预期并进行断言。与手工测试不同,自动化测试集的本质是,它们只会验证预期存在的缺陷——换句话说,它只会对预期内的返回做断言。举个最极端的例子,你可以创建一个自动化测试,启动要测试的软件,只需验证它可以成功运行,而不对程序输出内容做任何断言。这将是一个非常易于维护的测试,但也是一个价值非常低的测试,因为它除了告诉我们程序正常运行外,没有告诉我们其他内容。如果使用变异测试来评估这种假设软件和测试,得分将非常低,因为软件的变异版本只有在注入的缺陷导致软件完全运行失败时才会使测试报错。变异测试不能解决测试可维护性的问题,但它确实能对单个测试的实际价值提供一定的见解。这一点也很重要,因为即使在一个自动化程度很高的环境中,人们也往往不会执行全面的组合测试,因为它被认为是一种不切实际的资源使用方式。任何测试设计技术都存在一个风险,即测试用例爆炸,也就是说,当你专注于一种特定的测试设计技术时,测试的数量会急剧增加。
3. 代码覆盖率和变异运算符
变异测试是一种黑盒测试技术,测试人员做测试设计和执行不需要了解代码的内部执行逻辑,但是变异植入人员必须非常熟悉了解代码。事实上,生成的变异体与底层代码的语言和结构密不可分,真正的缺陷也是如此。当变异植入人员将缺陷注入代码,这里变异体通常可以由编写代码的开发工程师自定义,包括:
•从代码中删除语句
•从代码中插入语句
•更改代码中的条件
•替换变量
让我们回顾一些基本的编程概念以及它们与缺陷、代码覆盖率和变异测试的联系。在大多数编程语言中,可执行语句表示要执行的操作,例如将一个值(例如true或false)分配给一个变量。ISTQB术语表将此定义为:“当编译时,该语句被编译成目标代码,并且在程序运行时将按顺序执行,并且可能对数据执行操作。”例如,在下面的代码中,所有不以IF/ELSE开头的代码都是语句:
allowEntrance = false
if(customerHasMembershipCard or customerHasAccessCard):
allowEntrance = true
price = 0
else:
if(weekend):
allowEntrance = true
price = 10
可执行测试集覆盖语句的程度通常被称为语句覆盖率,语句覆盖率是测试集执行的语句的百分比。
语句覆盖率=执行语句数/总语句数
要达到上述代码的完整语句覆盖,你需要两个测试,因为要执行每个语句,需要通过代码中的两个互斥路径。另一种覆盖方法是覆盖每个分支或决策,基本上,只要包含条件语句(如if、for、while),就要确保条件语句的两个结果都被评估。要达到上述示例的完整分支覆盖,你需要一个额外的测试,以覆盖weekend变量是否为真或假。
最后,条件覆盖率是一种代码覆盖率度量标准,它衡量的是每个单个条件是否已被评估为真或假。这可以计算为:
因此,再次以上面的例子为例,确保测试覆盖了会员身份和访问卡场景,为我们不断增多的测试集添加另一个测试。仅使用代码覆盖率指标来衡量自动化测试质量的问题在于,这些指标没有一个可以评估我的测试是否实际上检查了客户是否被允许访问,或者软件计算了多少费用。这些状态和变量的验证不包括在指标中。在自动化测试中测量代码覆盖率固然很好,但这只是画面的一部分。代码覆盖率只告诉你已经执行的逻辑和分支,它不能真正衡量你的测试是否获得了大量的功能覆盖数据,也不能告诉你你的测试是否有效地检测到了缺陷。验证系统响应(有效比较实际结果与预期结果)是实现自动化测试的关键部分,直接检查一个变量是否合理是很容易的;然而,随着接口变得越来越复杂,围绕验证的设计主观性也在增加。变异操作引擎将变异操作符应用于代码,它们支持的操作符各不相同,实际上用户通常可以配置如何应用这些操作符。例如,著名的Mothra研究和支持工具使用了表1中所示的Fortran中的操作符。这些操作符有些过时,因为操作符随着面向对象技术的发展而发展。
当然,潜在操作符的数量及其导致的软件代码变异是巨大的,任何实现都不可能是穷尽的。通过一个变异操作符运行测试软件可能会导致代码中的许多变化。例如,基于规则的操作符应用于此代码:
allowEntrance = false
if (customerHasMembershipCard or customerHasAccessCard):
allowEntrance = true
price = 0
else:
if(weekend):
allowEntrance = true
price = 10
在这个例子中,红色项目将被更改。第一个将被初始化为true,第二个将把or改为and,第三个和第四个将通过添加not来否定布尔值。因此,将被编译出四个测试系统的版本,并且变异测试例程应该针对每个版本运行所有自动测试。为了成功检测每个变异体,测试集需要具有以下特征:
•测试如果客户没有两张卡,并且不是weekend,则不允许客户进入。这将测试变异1中未初始化变量的情况。
•测试如果客户有一张卡,但没有另一张卡(完整条件覆盖也需要测试这种情况),然后验证客户被允许进入。
•测试如果客户在weekend没有卡是否被允许进入。
•替换变量
正如你所看到的,尽管为达到代码覆盖率而构建的测试集会通过代码执行类似的路径,但变异测试指标允许对测试应执行的验证进行更具体的描述。毕竟大多软件缺陷是在编码过程中引入的。例如,常见的“下标偏移”缺陷,即程序员指示循环迭代一次或多次,或者太少——或者误算了边界条件——可以直接通过边界值分析和等价划分等测试设计技术来解决。同样,这种缺陷通常是由变异测试操作员注入的。变异测试过程可以概括如下:
1. 通过插入缺陷来创建变异体。
2. 变异体创建后,选择并执行测试。
3. 如果变异体执行测试时测试失败,则变异体将被“杀死”。
4. 如果变异体测试的结果与基础软件相同,则变异体“存活”。
5. 可以添加新的测试,修改现有的测试或重构代码,以增加“杀死”的变异体数量。一些变异体无法被检测到,因为它们会产生与测试的原始软件等价的输出,这些被称为“等价变异体”。一旦整个过程执行完毕,就可以计算变异体得分。这是杀死的变异体与变异体总数的比率。这个分数越接近1,测试集和软件的质量就越高。
变异得分=杀死的变异体数量/总变异体数量
4. 变异测试的挑战和策略
进行变异测试所需的成本比较高,而且需要大量花费时间来检查和修复发现的问题,需要考虑成本如下:
•生成变异体的编译时间成本
•在变异体上运行测试的运行时间成本
•分析结果的人力成本。
编译时间正如前面提到的,变异测试的一个主要问题是执行成本。变异体的数量是代码行数和数据对象数之积,但通常情况下,生成的变异体的数量通常是代码行数的平方。一些策略已经尝试减少执行量:
-
抽样——在软件的逻辑区域和相关测试中仅执行变异体的随机样本。
-
聚类——使用无监督机器学习算法(例如,K-means)来选择变异体。
-
选择性测试——减少变异体操作符的数量,即用于注入缺陷的启发式方法,可以减少变异体的数量60%。
-
高阶变异——一阶变异体是那些只注入一个缺陷的变异体;二阶变异体是在多次变异迭代中注入多个缺陷的变异体。高阶变异体更难杀死,而仅关注二阶变异体已被证明可以减少工作量,而不会减少覆盖率。
-
增量变异——只对变更代码进行变异测试,而不是测试中的整个代码库。
我们知道,缺陷具有集聚性,也许一个简单的策略,将技术应用于有限、复杂、高风险和充满缺陷的功能区域,可以提供成本与收益的适当平衡。相反,代码中有些语句我们不必担心。例如,从变异中排除所有日志语句可能是适当的,并导致更少的变异体进行测试。另一种减少所需时间和资源的方法是直接与编译器集成。早期的变异测试方法单独编译每个变异体;然而,更现代的方法编译一次,然后对中间形式(如字节码)进行变异。这在编译性能方面具有显着优势,但在评估每个变异体的执行时间方面没有优势。
运行时间运行变异测试周期的运行成本可能是巨大的。当然,在大多数情况下,执行可以水平扩展,因为测试可以在多台机器上运行,或者可以通过使用更强大的机器进行垂直扩展。为了应用水平可扩展性,有必要在选择适当的变异测试工具时考虑这一点,并支持此类方法。还需要考虑提高测试运行时间的经典测试自动化技术。当测试运行一次时,内置到测试中的硬编码等待可能不会引起注意,但当扩展到数百或数千次执行时,就会成为一项重要的成本。在尝试引入变异测试之前,应优化自动化测试以提高性能。
分析两个相关的挑战是Oracle问题以及减少等价变异的问题。Oracle问题远非变异测试所独有,它适用于任何难以确定测试是否预期的测试领域。当无法杀死变异体时,就会发生这种情况,因为需要在测试中实现的断言太难实现。当软件的确定性较低,并且难以理解检测不到变异是否实际上是有意义时,也会发生这种情况。那么最大的挑战是什么:
最大的问题是测试oracle问题。对于小型项目来说,这不是什么大问题。然而,对于大型项目来说,变异会产生数千个变异体,其中数百个是有效的。目前还不清楚开发工程师应该如何处理它们——人工review这些显然是不切实际的。
减少等价变异的问题也很重要,也就是说,变异不会导致输出可观察变化。这可能是因为未执行过的僵尸代码;变异只改变软件的速度;或者变异只改变内部使用的数据,不影响最终输出。
让我们看一个等价的变异:
def foo(i):
return i + 1 + 0
一个潜在的等价变异的例子是删除“+ 0”。虽然这会改变正在测试的软件,但它不会改变输出,因为向一个数字加零没有任何实际效果。这正是关键所在,代码无关紧要,应该删除。
5. 变异测试工具
变异测试工具的种类繁多,这在一定程度上是因为它们与实现中使用的编程语言有着内在的联系。你通常不能使用为 C++ 设计的工具来编写 Java。选择一个支持底层技术栈、支持让你配置所需的变异启发式方法、与你的开发环境集成并支持并行和分布式执行的工具至关重要。使用这些工具可以简单到在你的构建配置中注入依赖项。选择和评估适合你的工具实际上比传统工具选择更重要。一个原因是这些工具将带有不同的内置默认操作符和策略。正如前面提到的,这些操作符和策略有效地决定了技术方法,并直接影响结果。该工具还可以确定如何扩展运行时执行,而选择一个支持有限的工具可能会导致不切实际的时间表。
然而,你需要考虑的不仅仅是变异工具:
作为一个实施者,我看到的更大的问题应该是如何无缝集成。如何将变异集成到现有的基础设施中:各种构建系统、测试框架、CI 管道、IDE 等
对于测试周期而言,还有很多工具。例如,Pitest,一个Java变异引擎;Cucumber插件允许你与Cucumber行为驱动开发集成。同样,SonarQube,一个流行的代码质量监控集,有插件允许你显示变异测试运行的详细结果。一些变异测试引擎还提供IDE插件,加速反馈循环,并允许你在代码上下文中查看结果。
6. 总结一下
本文主要是希望开阔大家的视野,以另一种视角来看测试覆盖率。虽然变异测试仅频繁应用在单元测试层面,但这些概念可以应用于整个软件质量保证实践。它不仅是一种评估自动化测试价值的有效方法,而且是一种了解代码复杂性的方法,以及定量了解代码质量和测试覆盖率的方法。这些概念也可以应用于代码、输入数据、环境和其他技术。
- END -
下方扫码关注 软件质量保障,与质量君一起学习成长、共同进步,做一个职场最贵Tester!
关注「软件质量保障」微信公众号
好文推荐
往期推荐
聊聊工作中的自我管理和向上管理
经验分享|测试工程师转型测试开发历程
聊聊UI自动化的PageObject设计模式
细读《阿里测试之道》