T3i:用于生成和查询 Java 测试套件的工具

T3i:用于生成和查询 Java 测试套件的工具

摘要

T3i 是一款用于测试 Java 类的自动化单元测试工具。为了使用户可以在测试过程中与工具进行交互,T3i 将生成的测试用例组织为对目标类的方法的调用序列。T3i 与其他测试工具的不同之处在于它将测试套件视为第一类对象(The first class objects),并允许用户对它们执行例如合并、查询和过滤这些操作。通过这些操作,用户可以构建具有特定属性的测试套件。用户可使用查询来检查属性的正确性,比如 Hoare 三元组,LTL 公式和代数方程。用户可以定制 T3i 提供的各种操作,因而可以通过生成的脚本进行更多的探索性测试。用户可以使用 Java 语法来控制 T3i。此外,T3i 也支持其他轻量级语法,如 Groovy。

1 引言

​ T3i 是一款用于自动测试 Java 类的工具。给定一个待测类 C,T3i 将生成由 C 成员方法的随机调用序列组成的测试套件,这些测试套件可以触发该类内的方法交互(同类成员方法的交互在 OO 程序中很常见)。

​ 通常,自动化测试工具在持续不断地提供覆盖范围的过程中往往会产生一些问题。最近的一次工具比赛的结果显示:即使是得分最高的工具,也只能为基准测试的所有目标类(一共为 63 个)提供 50%的分支覆盖率。比赛中使用的所有的目标类在比赛开始前都是保密的,因此,开发人员没有办法事先将参加比赛的工具调整为适应这些目标类的最佳状态,工具使用的均为通用配置。实际上,工具可以为某些目标类提供高达 100%的分支覆盖率;但是就平均值而言,在无法对工具进行调整的情况下,工具生成的测试套件最多只能提供 50%的分支覆盖率。由此作者发现:调整工具的配置通常可以大大提高测试交付的质量。因此,T3i 的核心理念是提供一款易于用户控制和调整的自动化测试工具。

​ T3i 与其他自动化测试工具的不同之处在于,它将测试套件视为第一类对象(The first class objects)。T3i 提供诸如合并、查询和过滤之类的操作协助用户与工具进行交互,从而操纵测试套件的生成过程。通过这些操作,用户可以获得有关生成的测试套件的相关信息,并一步步对这些测试进行组合、过滤,以使得最终生成的测试套件满足一系列预期属性。T3i 后端依靠随机算法来实现测试序列的生成。该算法的效率很高,可以在一秒内生成数千个测试序列。用户在很短的时间内尝试不同的配置,并最终生成各自己预期的各种测试套件。

​ 除了简单的查询(例如查询测试套件执行某个方法或经历某种状态的次数)之外,用户还可以选择采用 Hoare 三元组、LTL 范式以及代数方程式的形式执行高级查询。此类查询可用于检验查询公式的有效性,或者用于从生成的套件中过滤出满足条件(如公式表达的前提条件)的方法序列。

2 架构

T3i:用于生成和查询 Java 测试套件的工具

图 1:T3i 架构

图 1 展示了 T3i 整体架构。其中,工具 T3 在后端完成测试序列的生成工作。T3i 包含查询和操作测试套件的操作层,以及完成 T3 配置的配置层。T3i 是一种领域特定语言(Domain Specific Language),由 Groovy 编写,因此用户可以选择通过 Groovy Shell 直接使用 T3i,也可以通过编写脚本来驱动 T3i。该脚本可以是 Groovy 脚本,也可以是 Java 脚本或 Junit 测试。T3i 生成的测试套件会保存在轨迹文件(后缀为.tr)文件中,以支持后续重加载或重放测试套件。

后端工具 T3 采用的随机算法是 Randoop 的一种变体。除此之外,T3i 还引入了“目标”的概念,即:每个测试序列的生成趋势都朝向一个既定的目标。这个目标可以是一个或一对待测方法。 工具生成的序列可以描述成 σ g τ 的形式,其中:g 是代表目标,前缀 σ 用于设置 g 的随机初始状态,而后缀 τ 则用于观测 g 执行过程中产生的副作用。

T3i:用于生成和查询 Java 测试套件的工具

图二:T3i 会话示例

图 2 展示了用户通过 Groovy-shell 与 T3i 进行交互式会话的示例,解释器响应信息中次要的部分已经从示例中略去。示例中,会话在第 1 行创建了一个配置,内容包括指定被测类(CUT)等操作;第 2 行,T3i 使用指定的配置创建后端 T3 的实例;第 3 行,T3i 调用 T3 进行测试套件的生成。T3 提供了两种方法——ADT()和 nonADT()——用于测试套件的生成。第一个方法用于测试 CUT 中的非静态成员,第二个用于测试静态成员。一个 ADT 操作序列总是始于创建 CUT 实例,这个 CUT 实例称为被测对象(oUT, object Under Test)。之后的相关操作均在这个 oUT 上进行。

第 7 和 9 行是对生成的测试套件的两个简单查询。第一条查询语句用于查询套件中调用了方法 foo 的测试序列个数;第二个查询语句则用于记录覆盖 oUT 的不同状态的序列个数,其中程序的状态特征用谓词{ o → o.x > 0}描述。o → o.x > 0 是 groovy 中的 λ 表达式。

3 测试套件的基本操作

用户可以通过表达式 S1 + S2 将两个测试套件 S1 和 S2 进行组合,这将产生一个由 S1 和 S2 的序列组成的全新测试套件。

为了支持测试套件的查询和过滤,T3i 引入了 Queriable 类。一个 Queriable 类的实例对象 q 表示一个可查询序列的集合。我们可以通过 q.data 直接引用这个集合,也可以使用一下方法操作这个集合:(1) 使用 q.collect()会获取 q.data; (2) 使用 q.count()获取 q.data 的规模; (3) 使用 q.sat()对可查询序列集合的状态进行判断:如果 q.count() > 0,则返回 true;(4) 假设 φ 是一个序列谓词,则 q.with(φ)会在 q 的基础上生成一个新的 Querieable 实例,这个实例的可查询集合由 q.data 中满足 φ 的序列组成。也就是说,q.with(φ)表示用谓词 φ 来对 q 中的可查询集合进行过滤,从而获取到与当前需求相关的方法序列。

如果 S 是一个普通的测试套件,则 query(S)会将其转换为一个 Queriable 实例。 例如,下面的第一个表达式用于检查 φ 是否满足 S 的测试需求;第二个表达式则用于将 S 中所有满足谓词 φ 的序列搜集起来,组成一个新的测试套件 S’。

T3i:用于生成和查询 Java 测试套件的工具

用户也可以通过下列代码来验证谓词 φ 在测试套件上的可用性:

T3i:用于生成和查询 Java 测试套件的工具

但是上述写法比较冗长、可读性较差。用户可以选择使用 data 的补集(即那些不满足查询谓词的序列)的方式更加简洁地表达上述语义。方法 validate()可用于检查某个序列集合的补集是否为空。由此,上面的查询就可以写成:

T3i:用于生成和查询 Java 测试套件的工具

图二中展示的方法 visit()可以用于构造序列谓词。例如,visit(name)可以构造一个用于描述名为 name 的方法或者构造器的被调用状态的谓词,即当序列中包含对 name 的调用时 visit(name)的值就为 true。文章的后续部分会介绍更多用于构造谓词的函数。

​ 我们还可以通过一些操作实现测试套件的变换。假设 f 是一个序列到序列的函数,则 q.transform(f)会产生一个新的 Queriable 实例。这个实例的 data 由 f(σ)组成,其中 σ 表示原始序列 q 的 data 中的可查询序列。同时,新产生的 Queriable 实例的 data 只包含非空的(可以成功执行的)方法序列 f(σ)。例如:下面这个表达式将套件 S 中包含的每个测试序列 σ 中出现的第一个“f(), g()”调用序列转变成“g(), f()”,并施加过滤操作以保证生成的序列全部可用。

T3i:用于生成和查询 Java 测试套件的工具

4 高级查询

用户也可以在 T3i 中使用 Hoare 三元组规范(即规定方法的前置条件和后置前提)。下面展示了关于方法 f(x)的两个规范。第一个变量 H1 表示:如果 x 不为空,则 f 不会抛出任何异常;而 H2 则表示:如果 x 为空,则该方法会抛出一个异常。

T3i:用于生成和查询 Java 测试套件的工具

Hoare 三元组也可以用作序列谓词。比如,当想要检查 H2 在测试套件 S 上的有效性时,我们可以简单地使用下列表达式:

T3i:用于生成和查询 Java 测试套件的工具

一种更强大的序列谓词表达形式是线性时间逻辑(LTL, Linear Temporal Logic)范式[1]。 假设 φ 是一个 LTL 范式,用户可以使用 T3i 提供的运算符构造更复杂的 LTL 公式,相关运算符如下所示:

T3i:用于生成和查询 Java 测试套件的工具

例如,eventually(always(φ))表示这样一个序列谓词:当序列的末尾是 φ 并且一直为 φ 的情况下,该谓词的评估结果就为 true。

5 生成测试套件

当用户在使用一些开箱即用的自动化测试工具时,经常会出现自动生成的测试用例不能够达到预期的覆盖要求的现象。这种现象屡见不鲜,其根本原因在于测试问题的不确定性。借助一定的人工调整可以大大改善自动化测试生成的测试交付,这种“人工调整”的本质在于利用工作人员的洞察力以指导工具更好地完成测试生成。设想:一个 CUT 中定义了一个方法 add(String email),这个方法需要传入一个格式正确的、用于表示电子邮件的字符串。对于仅仅依赖随机策略进行测试生成的工具来说,这是一件非常困难的事情。T3i 则允许用户将一些预定义的值生成器交付给后端序列生成器(T3),以协助 T3 完成一些特定的生成任务。每当测试序列中的某一步有特殊需求(如需要一个关于特定方法 m(x)的调用)时,后端序列生成器通常会利用 T3 内置的值生成器来生成该特殊需求。用户可以通过一组简单表达式轻松地定制一个值生成器,如下所示:

T3i:用于生成和查询 Java 测试套件的工具

上述表达式的语法格式与 QuickCheck 比较相似。但是,由于测试序列是随机生成的,单纯依赖随机策略的测试生成工具时不可能将值生成器与目标待测类中的特定方法对应联系起来的,这也是 T3i 与其他随机测试生成工具最显著的差异。QuickCheck 在测试生成过程中不接受任何其他输入,而 T3i 会在生成特殊值前先接收一个请求。随后,T3i 检查该请求以确保自己生成的值与用户需求相匹配。由于用户信息可以这种交互请求中进行编码大量的信息(如待生成的参数的名称),T3i 的测试生成功能理论上更强大。

由此,用户可以使用以下方式创建使用上述 G 的 T3 实例:

T3i:用于生成和查询 Java 测试套件的工具

然后,如图 2 中的示例所示,我们可以通过调用 t3’.ADT()来生成套件。

当后端生成器需要一个 email 字符串作为参数时,G 将随机地从上面指定的电子邮件中选出一个,并交付后端生成器;同时,G 中的第二条指明了名为 region 的参数可选值列表。当生成过程中需要实例化一个 region 参数时,该参数的值就从这个列表中选取。这种选择由采用了同一分布(Uniform Distribution)的函数表达式 OneOf(…)来完成。这个表达式会构造一个 Supplier类(Java 8 提供的一个函数式接口)的实例来完成相应的生成任务。我们可以通过以下方式自定义一个 OneOf 方法的变体,如定义一种使用高斯分布的 OneOf 方法,如下所示:

T3i:用于生成和查询 Java 测试套件的工具

致谢

本文由南京大学软件学院 2020 级硕士生钱瑞祥翻译转述。

感谢国家重点研发计划(2018YFB1003900)和国家自然科学基金(61832009,61932012)支持!


分享到:


相關文章: