在上一章中,我们介绍了基本的模糊测试,即生成随机输入来测试程序。我们如何衡量这些测试的有效性?一种方法是检查发现的错误的数量(和严重性);但是,如果 bug 很少,我们需要一个代理来衡量测试发现 bug 的可能性。在本章中,我们将介绍代码覆盖率的概念,它衡量在测试运行期间实际执行程序的哪些部分。对于试图覆盖尽可能多的代码的测试生成器来说,测量这种覆盖率也至关重要。
这段代码定义了一个名为 cgi_decode
的函数,它用于解码CGI(Common Gateway Interface)编码的字符串。CGI编码通常用于URL编码,以便在URL中安全地传输特殊字符。函数接收一个字符串作为输入,并返回解码后的字符串。
函数内部实现了以下功能:
-
定义hex_values字典:该字典用于将十六进制字符('0'-'9', 'a'-'f', 'A'-'F')映射到它们对应的十进制整数值。
-
解码处理:函数通过遍历输入字符串的每一个字符来进行解码。
- 如果遇到加号('+'),则将其替换为一个空格字符。
- 如果遇到百分号('%'),则检查其后的两个字符是否都是有效的十六进制字符。如果是,则计算这两个十六进制字符对应的十进制值,并将其转换为对应的字符。然后将这个字符添加到解码后的字符串中。如果百分号后的两个字符不是有效的十六进制字符,则抛出一个
ValueError
异常,表示无效的编码。 - 如果遇到其他字符,则直接将其添加到解码后的字符串中。
-
返回解码后的字符串:当遍历完输入字符串的所有字符后,函数返回解码后的字符串。
这个函数在处理URL或其他需要编码的文本时非常有用,因为它能够正确地将CGI编码的字符串转换回原始的、可读的字符串。
---------------------------------------------------------------------------------------------------------------------------------
以下是工作原理的示例:cgi_decode()
cgi_decode("Hello+world")
'Hello world'
如果我们想系统地测试,我们将如何进行?cgi_decode()
测试文献区分了两种推导测试的方法:黑盒测试和白盒测试
黑盒测试的优点是它可以在指定行为中发现错误。它独立于给定的实现,因此允许在实现之前创建测试。缺点是,实现的行为通常比指定的行为涵盖更多的领域,因此仅基于规范的测试通常不能涵盖所有实现细节。
白盒测试
与黑盒测试相比,白盒测试从实现中派生测试,尤其是内部结构。白盒测试与覆盖代码结构特征的概念密切相关。例如,如果代码中的某个语句在测试期间未执行,则意味着该语句中的错误也无法触发。因此,白盒测试引入了许多覆盖标准,在测试可以说是充分的之前,必须满足这些标准。最常用的承保标准是
- 语句覆盖率 – 代码中的每个语句必须由至少一个测试输入执行。
- 分支覆盖率 – 代码中的每个分支必须由至少一个测试输入获取。(这意味着每个和决定一次是真的,一次是假的。
if
while
除此之外,还有更多的覆盖率标准,包括所采用的分支序列、所采用的循环迭代(零、一、多)、变量定义和用法之间的数据流等等;[Pezzè et al, 2008] 有一个很好的概述。
白盒测试的优点是它可以发现已实现行为中的错误。即使在规范没有提供足够细节的情况下,也可以进行;实际上,它有助于识别(从而指定)规范中的极端情况。缺点是它可能会错过未实现的行为:如果缺少某些指定的功能,白盒测试将找不到它。
跟踪执行
白盒测试的一个很好的功能是,人们实际上可以自动评估是否涵盖了某些程序功能。为此,可以检测程序的执行,以便在执行过程中,一个特殊的功能跟踪执行了哪些代码。测试后,这些信息可以传递给程序员,然后程序员可以专注于编写涵盖尚未发现的代码的测试。
在大多数编程语言中,设置程序以便可以跟踪其执行是相当困难的。在 Python 中并非如此。该函数允许定义一个跟踪函数,该函数为执行的每一行调用。更好的是,它可以访问当前函数及其名称、当前变量内容等。因此,它是动态分析的理想工具,即分析执行过程中实际发生的情况。
这些代码主要用于追踪Python程序的执行过程,特别是cgi_decode
函数的执行。通过使用sys.settrace()
函数和自定义的追踪函数traceit
,我们可以收集到cgi_decode
函数执行过程中访问的每一行代码的行号,并将这些行号存储在全局变量coverage
中。
以下是代码的简要介绍:
- 定义追踪函数
traceit
:- 接收三个参数:
frame
(当前执行帧的引用)、event
(追踪事件类型)和arg
(与事件相关的参数)。 - 当事件为
'line'
(即代码中的一行被执行)时,将当前执行的函数名和行号添加到全局列表coverage
中。 - 错误地返回了
traceit
函数自身,这实际上会导致无限递归(应该返回None
)。
- 接收三个参数:
- 定义包装函数
cgi_decode_traced
:- 这个函数接受一个字符串
s
作为参数,并用于调用cgi_decode
函数,但在调用过程中进行追踪。 - 在调用
cgi_decode
之前,重置coverage
列表并开启追踪(通过sys.settrace(traceit)
)。 - 在调用
cgi_decode
之后,关闭追踪(通过sys.settrace(None)
)。
- 这个函数接受一个字符串
- 测试追踪:
- 使用
cgi_decode_traced("a+b")
调用包装函数,并传入字符串"a+b"
作为参数。 - 追踪过程中收集到的行号被存储在
coverage
列表中。 - 打印
coverage
列表,以显示cgi_decode
函数执行时访问的行号。
- 使用
实际上,这些是哪一行?为此,我们获取源代码并将其编码成一个数组,然后我们将用覆盖率信息对其进行注释。省略中间代码,目的为了在源代码中显示出那些代码没有被执行覆盖。
---------------------------------------------------------------------------------------------------------------------------------
Coverage
上面的内容描述了一个使用Python的with
语句来管理资源(在这种情况下是代码覆盖率追踪)的概念。with
语句是Python中用于确保代码块执行完毕后进行清理操作(如关闭文件、释放锁等)的上下文管理协议的一部分。这个协议定义了两个方法:__enter__
和__exit__
,它们分别在进入和退出with
语句块时自动调用。
这里的关键点在于,你描述了一个名为Coverage
的类,这个类可能用于追踪代码覆盖率。这个类实现了__enter__
和__exit__
方法,以便在with
语句块开始时启动追踪,并在块结束时停止追踪。同时,该类可能还提供了一个方法(例如coverage
)来获取追踪结果。
Coverage类的具体代码实现省略,上图为使用覆盖类的代码。
多次进行随机输入的覆盖率,中间代码省略,画图如下图所示。
---------------------------------------------------------------------------------------------------------------------------------
C语言程序可以实现同样的内容,代码见书。
在笔记本执行的时候改了两行代码,因为不是linux系统,所以原有的两行代码无法执行。
有趣的是,我们之前设计的所有手动测试都不会触发这个错误。实际上,无论是声明还是分支覆盖率,以及文献中通常讨论的任何覆盖率标准都找不到它。但是,简单的模糊测试运行可以通过几次运行来识别错误 - 如果适当的运行时检查已到位以发现此类溢出。这肯定需要更多的模糊测试!
经验 教训
- 覆盖率指标是一种简单且完全自动化的方法,用于估算在测试运行期间实际执行的程序功能量。
- 存在许多覆盖率指标,其中最重要的是报表覆盖率和分支机构覆盖率。
- 在 Python 中,在执行过程中非常容易访问程序状态,包括当前执行的代码。
后续步骤
覆盖率不仅是衡量测试有效性的工具,也是指导测试生成实现特定目标(尤其是未覆盖代码)的绝佳工具。我们使用 coverage 来
---------------------------------------------------------------------------------------------------------------------------------
练习