现代化和转型遗留应用程序是一项具有挑战性的活动,涉及多项任务。其中一项关键任务是验证现代化后的应用程序是否保留了遗留应用程序的功能。遗憾的是,这可能既繁琐又难以执行。遗留应用程序通常没有自动化测试用例,或者,即使有,测试覆盖率也可能不足,无论是总体上还是专门针对与现代化相关的更改。维护不善的测试套件也可能包含许多过时的测试(随着应用程序的发展而累积)。因此,在大多数现代化项目中,验证主要通过手动完成——这是一个耗时的过程,并且可能无法充分测试应用程序。在一些报告的案例研究中,测试占现代化项目花费时间的约 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 的高级组件。
图 1 显示了 Tackle-test 主要组件的高级视图。它由一个基于 Java 的核心测试生成器(生成 CTD 驱动的测试)和一个基于 Python 的命令行界面 (CLI) 组成,命令行界面是用户交互的主要机制。
工具入门
Tackle-test 以开源形式在 Konveyor 组织下发布 (https://github.com/konveyor/tackle-test-generator-cli)。要开始使用,请克隆 repo,并按照 repo 自述文件中提供的说明安装和运行该工具。有两种安装选项:使用 docker/docker-compose 或本地安装。
CLI 提供两个主要命令:generate
用于生成 JUnit 测试用例,execute
用于执行它们。要验证您的安装是否成功完成,请使用 test/data 文件夹中的示例 irs
应用程序来运行这两个命令。
generate
命令附带一个子命令,用于指定测试生成策略(ctd-amplified
、randoop
或 evosuite
),并创建 JUnit 测试用例。默认情况下,差异断言会添加到生成的测试用例中。让我们在 irs
示例上运行 generate 命令,使用 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<irs.Salary> salaryList1 = new java.util.ArrayList<irs.Salary>();
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<irs.Salary>)salaryList1);
}
此测试方法旨在测试 IRS 的 setSalaryList
方法,该方法接收 irs.Salary
对象列表作为其输入。我们可以看到,测试用例的语句后面是调用 assertEquals
方法,将生成的对象的值与在此测试生成期间记录的值进行比较。当测试再次执行时,例如,在应用程序的现代化版本上,如果任何值与记录的值不同,则会发生断言失败,这可能表明代码已损坏,未保留遗留应用程序的功能。
接下来,我们将使用 CLI execute
命令编译并运行生成的测试用例。我们注意到,这些是标准的 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 测试方法都通过了。这是预期的,因为我们在生成它们的同一应用程序版本上执行了它们。

从也位于 irs-tkltest-reports
中的 JaCoCo 代码覆盖率报告中,我们可以看到,CTD 指导的测试生成在 irs 示例上实现了总体 71% 的语句覆盖率和 94% 的分支覆盖率。我们还可以深入到类和方法级别,查看它们的覆盖率。缺失的覆盖率是测试计划行的结果,测试生成器无法为其生成通过序列。增加每个类的测试生成时间限制可以提高覆盖率。

CTD 指导的测试生成
图 2 说明了在 Tackle-test 的核心测试生成引擎中实现的 CTD 指导的测试生成流程。测试生成流程的输入是 (1) 应用程序类、(2) 应用程序的库依赖项和 (3) 可选的要针对测试生成的应用程序类集(如果未指定,则针对所有应用程序类)的规范。此规范通过 TOML 配置文件提供。流程的输出包括:(1) JUnit 测试用例(带或不带断言)、(2) Maven 和 Ant 构建文件和 (3) 包含测试生成和 CTD 测试计划覆盖率摘要的 JSON 文件。

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

图 3:使用不同策略和交互级别为取自 SF110 基准的两个小型开源 Java 应用程序生成的测试用例实现的指令覆盖率。
图 3 显示了使用不同测试策略在两个小型开源 Java 应用程序上生成的测试所实现的语句覆盖率数据。这些应用程序取自 SF110 基准,这是一个大型开源 Java 应用程序语料库,旨在促进自动化测试技术的实证研究。其中一个应用程序 jni-inchi
包含 24 个类和 74 个方法;另一个应用程序 gaj
包含 14 个类和 17 个方法。箱线图显示,仅通过针对 CTD 测试计划行本身就可以实现良好的语句覆盖率,并且与与从 Randoop 和 EvoSuite 生成的测试用例中采样的 CTD 指导的测试套件大小相同的测试套件相比,CTD 指导的测试套件实现了更高的语句覆盖率,使其效率更高。
目前正在使用来自 SF110 基准的更多应用程序和一些专有的企业 Java 应用程序对 Tackle-test 进行大规模评估。
如果您希望观看视频演示,可以点击此处观看。
我们鼓励您试用该工具并提供反馈,以帮助我们通过提交拉取请求来改进它。我们还邀请您通过为项目做出贡献来帮助改进该工具。
使用 Konveyor 社区迁移到 Kubernetes
Tackle-test 是 Konveyor 社区的一部分。该社区正在通过构建工具、识别模式以及提供关于分解单体应用、采用容器和拥抱 Kubernetes 的建议,帮助其他人实现现代化并将他们的应用程序迁移到混合云。
该社区包括将虚拟机迁移到 KubeVirt、Cloud Foundry 或 Docker 容器迁移到 Kubernetes,或在 Kubernetes 集群之间迁移命名空间的开源工具。这些是我们解决的一些用例。
有关这些工具的更新以及参加实践者展示他们如何迁移到 Kubernetes 的聚会的邀请,请加入社区。
参考文献
[1] COBOL to Java and Newspapers Still Get Delivered, https://arxiv.org/pdf/1808.03724.pdf, 2018。
[2] D. R. Kuhn, R. N. Kacker, and Y. Lei. Introduction to Combinatorial Testing. Chapman & Hall/CRC, 2013。
2 条评论