现代化和改造遗留应用程序是一项具有挑战性的活动,涉及到多项任务。其中一项关键任务是验证现代化的应用程序是否保留了遗留应用程序的功能。不幸的是,这可能是繁琐的,难以执行的。遗留应用程序通常没有自动测试用例,或者,如果有的话,测试覆盖率可能是不充分的,无论是一般的还是专门用于覆盖现代化相关的变化。一个维护不善的测试套件也可能包含许多过时的测试(随着时间的推移,应用程序的发展而积累)。因此,在大多数现代化项目中,验证主要是由人工完成的--这是一个耗时的过程,而且可能无法充分地测试应用程序。在一些报道的案例研究中,测试约占现代化项目花费时间的70%到80%[1]。Tackle-test是一个自动测试工具,旨在解决这一挑战。
Tackle-test的概述
Tackle-test的核心是为Java应用程序自动生成单元测试案例。它可以生成带有断言的测试,这使得该工具在现代化项目中特别有用,在这些项目中,应用程序的转换通常是保留功能的--因此,可以通过观察传统应用程序版本的运行时状态来创建有用的测试断言。这可以使传统的和现代化的应用程序版本之间的差异测试更加有效;没有断言的测试用例将只检测那些现代化的版本在测试输入上崩溃的差异,而传统版本则成功执行。Tackle-test生成的断言在每条代码语句后捕获创建的对象值,正如下一节所说明的。
Tackle-test使用一种新颖的测试生成技术,将组合测试设计(CTD)--也称为组合测试或组合交互测试[2]--应用于方法界面,目的是对具有 "复杂界面 "的方法进行严格的测试,其中界面的复杂性在一个方法可以被调用的参数类型组合的空间中得到表征。CTD是一种著名的、有效的、高效的测试设计技术。它通常需要以CTD模型的形式手动定义测试空间,由一组参数、它们各自的值和对值组合的约束组成。测试空间中的有效测试被定义为对每个参数分配一个满足约束条件的值。CTD算法自动构建有效测试集的一个子集,以涵盖每t个参数的所有合法值组合,其中t 通常是用户输入的。
尽管CTD通常以黑箱方式应用于程序输入,并且CTD模型是手动创建的,但Tackle-test为每个被测方法自动建立了一个基于参数类型的白箱CTD模型。然后,它生成一个由模型的覆盖目标组成的测试计划,并合成测试计划的覆盖行的测试序列。测试计划可以在不同的、用户可配置的交互水平上生成,更高的水平导致生成更多的测试用例和更彻底的测试,但代价是增加测试生成的时间。
Tackle-test还利用一些现有的和常用的测试生成策略来最大化代码覆盖率。具体来说,这些策略包括反馈驱动的随机测试生成(通过Randoop开源工具)和基于进化和约束的测试生成(通过EvoSuite开源工具)。这些工具计算代码元素的覆盖目标,如方法、语句和分支。

图1:Tackle-test的高层次组件。
(Saurabh Sinha and Rachel Tzoref-Brill,CC BY-SA 4.0)
图1展示了Tackle-test的主要组成部分的高层视图。它由一个基于Java的核心测试生成器和一个基于Python的命令行界面(CLI)组成,后者是用户互动的主要机制。
开始使用该工具
Tackle-test是在Konveyor组织(https://github.com/konveyor/tackle-test-generator-cli)下作为开源发布的。要开始使用,请克隆该版本,并按照该版本readme中提供的说明安装和运行该工具。有两个安装选项:使用docker/docker-compose或本地安装。
CLI提供了两个主要命令:generate ,用于生成JUnit测试用例,execute ,用于执行这些用例。为了验证你的安装成功完成,使用位于test/data文件夹中的样本irs 应用程序来运行这两条命令。
generate 命令伴随着一个指定测试生成策略的子命令(ctd-amplified,randoop, 或evosuite )并创建JUnit测试案例。默认情况下,diff断言被添加到生成的测试案例中。让我们在irs 样本上运行生成命令,使用CTD指导的策略。
$ tkltest --config-file ./test/data/irs/tkltest_config.toml --verbose generate ctd-amplified
[tkltest|18:00:11.171] Loading config file ./test/data/irs/tkltest_config.toml
[tkltest|18:00:11.175] Computing coverage goals using CTD
* CTD interaction level: 1
* Total number of classes: 5
* Targeting 5 classes
* Created a total of 20 test combinations for 20 target methods of 5 target classes
[tkltest|18:00:12.816] Computing test plans with CTD took 1.64 seconds
[tkltest|18:00:12.816] Generating basic block test sequences using CombinedTestGenerator
[tkltest|18:00:12.816] Test generator output will be written to irs_CombinedTestGenerator_output.log
[tkltest|18:01:02.693] Generating basic block test sequences with CombinedTestGenerator took 49.88 seconds
[tkltest|18:01:02.693] Extending sequences to reach coverage goals and generating junit tests
* === total CTD test-plan coverage rate: 90.00% (18/20)
* Added a total of 64 diff assertions across all sequences
* wrote summary file for generation of CTD-amplified tests (JSON)
* wrote 5 test class files to "irs-ctd-amplified-tests/monolithic" with 18 total test methods
* wrote CTD test-plan coverage report (JSON)
[tkltest|18:01:06.694] JUnit tests are saved in ./irs-ctd-amplified-tests
[tkltest|18:01:06.695] Extending test sequences and writing junit tests took 4.0 seconds
[tkltest|18:01:06.700] CTD coverage report is saved in ./irs-tkltest-reports/ctd report/ctdsummary.html
[tkltest|18:01:06.743] Generated Ant build file ./irs-ctd-amplified-tests/build.xml
[tkltest|18:01:06.743] Generated Maven build file ./irs-ctd-amplified-tests/pom.xml
在irs 样本上,测试生成需要几分钟的时间。默认情况下,该工具在初始测试序列生成时每类花费10秒。然而,由于额外的步骤,总的运行时间可能更长,这在下一节中解释。请注意,每个类的时间限制选项是可配置的,对于大型应用,测试生成可能需要几个小时。因此,在对所有应用程序类进行测试生成之前,从几个类的有限范围开始,以获得对该工具的感觉是一个好的做法。
测试生成完成后,测试用例被写入一个指定的目录,名为irs-ctd-amplified-tests ,作为工具的输出,同时还有用于编译和执行的Maven和Ant脚本。测试用例在一个名为monolith 的子目录下。每个应用类都会有一个单独的测试文件。每个测试文件都包含多种测试方法,用于测试CTD测试计划中规定的、具有不同参数类型组合的类的公共方法。一个CTD覆盖率报告被创建,总结了测试计划的部分,这些部分的单元测试可以在一个名为irs-tkltest-reports 的目录中生成。在上面的输出中,我们可以看到Tackle-test为20个测试计划行中的18个创建了测试用例,从而使测试计划覆盖率达到90%。

现在让我们看一下为irs.IRS 类生成的一个测试方法。
@Test
public void test1() throws Throwable {
irs.IRS iRS0 = new irs.IRS();
java.util.ArrayList salaryList1 = new java.util.ArrayList();
irs.Salary salary5 = new irs.Salary(0, 0, (double)100);
assertEquals(0, ((irs.Salary) salary5).getEmployerId());
assertEquals(0, ((irs.Salary) salary5).getEmployeeId());
assertEquals(100.0, (double) ((irs.Salary) salary5).getSalary(), 1.0E-4);
boolean boolean6 = salaryList1.add(salary5);
assertEquals(true, boolean6);
iRS0.setSalaryList((java.util.List)salaryList1);
}
这个测试方法打算测试IRS的setSalaryList 方法,它接收一个irs.Salary 对象的列表作为其输入。我们可以看到,测试用例的语句后面是对assertEquals 方法的调用,将生成的对象的值与这个测试的生成过程中记录的值进行比较。当测试再次执行时,例如在应用程序的现代化版本上,如果有任何值与记录的值不同,就会发生断言失败,这可能表明没有保留传统应用程序功能的破碎代码。
接下来,我们将使用CLIexecute命令编译和运行生成的测试案例。我们注意到,这些是标准的JUnit测试用例,可以在IDE中或使用任何JUnit测试运行器运行;它们也可以被集成到CI管道中。当用CLI执行时,会生成JUnit报告,也可以选择代码覆盖率报告(用JaCoCo创建)。
$ tkltest --config-file ./test/data/irs/tkltest_config.toml --verbose execute
[tkltest|18:12:46.446] Loading config file ./test/data/irs/tkltest_config.toml
[tkltest|18:12:46.457] Total test classes: 5
[tkltest|18:12:46.457] Compiling and running tests in ./irs-ctd-amplified-tests
Buildfile: ./irs-ctd-amplified-tests/build.xml
delete-classes:
compile-classes_monolithic:
[javac] Compiling 5 source files
execute-tests_monolithic:
[mkdir] Created dir: ./irs-tkltest-reports/junit-reports/monolithic
[mkdir] Created dir: ./irs-tkltest-reports/junit-reports/monolithic/raw
[mkdir] Created dir: ./irs-tkltest-reports/junit-reports/monolithic/html
[jacoco:coverage] Enhancing junit with coverage
...
BUILD SUCCESSFUL
Total time: 2 seconds
[tkltest|18:12:49.772] JUnit reports are saved in ./irs-tkltest-reports/junit-reports
[tkltest|18:12:49.773] Jacoco code coverage reports are saved in ./irs-tkltest-reports/jacoco-reports
Ant脚本默认执行单元测试,但用户可以将该工具配置为使用Maven代替。Gradle也将很快被支持。
查看位于irs-tkltest-reports 的JUnit报告,我们可以看到所有JUnit测试方法都通过了。这是意料之中的,因为我们是在生成这些方法的同一版本的应用程序上执行的。

从JaCoCo代码覆盖率报告(也位于irs-tkltest-reports )中,我们可以看到CTD指导的测试生成在IRS样本上实现了总体71%的语句覆盖率和94%的分支覆盖率。我们还可以深入到类和方法的层面来查看其覆盖率。缺少的覆盖率是测试计划行的结果,测试生成器无法生成一个合格的序列。增加每个类的测试生成时间限制可以提高覆盖率。

CTD指导的测试生成
图2说明了CTD指导下的测试生成流程,在Tackle-test的核心测试生成引擎中实现。测试生成流程的输入是:(1)应用程序类,(2)应用程序的库依赖,(3)可选的,测试生成的目标应用程序类集(如果没有指定,所有应用程序类都是目标)。这个规格是通过TOML配置文件提供的。流程的输出由以下部分组成(1) JUnit 测试用例(含或不含断言),(2) Maven 和 Ant 构建文件,以及 (3) 包含测试生成和 CTD 测试计划覆盖率总结的 JSON 文件。

图2:CTD指导下的测试生成过程。
(Saurabh Sinha 和 Rachel Tzoref-Brill,CC BY-SA 4.0)
该流程从CTD测试计划的生成开始。这包括为目标类的每个公共方法创建一个CTD模型。每个方法的CTD模型为该方法的每个形式参数捕获所有可能的具体类型,包括可以添加到集合/映射/数组参数类型的元素。Tackle-test结合了轻量级静态分析来推断每个方法的每个参数的可行的具体类型。
接下来,一个CTD测试计划在给定的(用户可配置的)交互水平上从模型中自动生成。测试计划中的每一行都描述了一个具体的参数类型组合,该方法应该被调用。默认情况下,交互级别被设置为一,这将导致单向测试:每个可能的具体参数类型至少出现在测试计划的一行。将交互级别设置为二,也就是成对测试,会导致测试计划中至少有一行包括每一对方法参数的每一对具体类型。
CTD测试计划提供了一组覆盖目标,需要为其合成测试序列。Tackle-test分两步完成这一工作。在第一步,它使用Randoop和/或EvoSuite(用户可以配置使用哪些工具)来创建基本测试序列。对基础测试序列进行分析,在方法和类的层面上生成序列池,测试生成引擎从中取样序列,为每个测试计划行组成一个覆盖序列。如果一个覆盖序列被成功创建,引擎会执行它,以确保该序列是有效的,即它不会导致应用程序崩溃。在这个执行过程中,还记录了所创建对象的运行状态,以便以后用于生成断言。失败的序列被丢弃。如果用户指定了断言选项,引擎会将断言添加到通过的序列中。最后,引擎将按类分组的序列导出到JUnit类文件。该引擎还创建了Antbuild.xml 和Mavenpom.xml 文件,如果需要,可以用来运行生成的测试案例。
其他工具特点
Tackle-test具有高度的可配置性,并提供了几个配置选项,用户可以使用这些选项来定制工具的行为:例如,为哪些类生成测试,使用哪些工具来生成测试,在测试生成上花费多少时间,是否向测试用例添加断言,生成CTD测试计划时使用什么交互级别,对扩展的测试序列执行多少次,等等。
不同测试生成策略的有效性
Tackle-test已经在几个开源的Java应用程序上进行了评估,目前也正在应用于企业级Java应用程序。

图3:对于取自 SF110基准的两个小型开源Java应用程序,使用不同策略和交互水平生成的测试案例所达到的指令覆盖率。
(Saurabh Sinha 和 Rachel Tzoref-Brill,CC BY-SA 4.0)
图3显示了在两个小型开源Java应用程序上使用不同测试策略生成的测试所实现的语句覆盖率数据。这些应用程序来自 SF110基准,这是一个大型的开源Java应用程序的语料库,旨在促进自动测试技术的经验研究。其中一个应用程序,jni-inchi ,由24个类和74个方法组成;另一个,gaj ,由14个类和17个方法组成。框图显示,针对CTD的测试计划行本身就可以实现良好的语句覆盖率,与从Randoop和EvoSuite生成的测试用例中抽出的与CTD指导的测试套件相同规模的测试套件相比,CTD指导的测试套件实现了更高的语句覆盖率,使其更有效率。
目前正在对Tackle-test进行大规模评估,使用SF110基准的更多应用和一些专有的企业Java应用。
如果你喜欢看视频演示,你可以在这里观看。
我们鼓励你试用这个工具,并通过提交拉动请求来提供反馈,帮助我们改进它。我们也邀请你通过对项目的贡献来帮助改进这个工具。
与Konveyor社区一起迁移到Kubernetes
Tackle-test是Konveyor社区的一部分。这个社区通过建立工具、识别模式以及提供关于打破单体、采用容器和拥抱Kubernetes的建议,帮助其他人实现现代化并将他们的应用迁移到混合云。
这个社区包括将虚拟机迁移到KubeVirt、Cloud Foundry或Docker容器到Kubernetes的开源工具,或Kubernetes集群之间的命名空间。这些是我们解决的几个用例。
参考文献
[1] 从COBOL到Java,报纸还是要送的, arxiv.org/pdf/1808.03…2018。
[2] D. R. Kuhn, R. N. Kacker, and Y. Lei.Combinatorial Testing简介.Chapman & Hall/CRC, 2013。