Java 自动单元测试生成框架
测试框架
- BDD(Behaviour-Driven Development):行为驱动开发
- TDD(Test-Driven Development):测试驱动开发
框架 | 类型 | 说明 |
---|---|---|
JUnit | TDD | 最基本的测试框架,可以与 Selenium 一同使用 |
JBehave | BDD | 主要与 Selenium WebDriver 一起使用 |
Serenity | BDD | 可以与 JBehave、JUnit 集成 |
TestNG | TDD | 主要使用 XML 配置,集成端到端测试 |
Mockito | TDD | 通过反射模拟对象,可以与 TestNG、JUnit 集成 |
HttpUnit | BDD | 基于 JUnit 开发,网站测试 |
JWebUnit | BDD | 网站测试 |
Gauge | BDD | 网站测试 |
Geb | BDD | 网站测试 |
Selenide | BDD | 由 Selenium 提供支持,网站测试 |
Spock | BDD | 网站测试 |
Cucumber | BDD | 支持 Java、JS、Ruby 等多种言 |
EvoSuite | 自动生成 JUnit 单元测试 | |
Randoop | 自动生成 JUnit 单元测试 | |
CATG | concolic 测试引擎,使用 ASM 进行插桩,依赖 python2.7,18 年停止更新 | |
Tackle-test | 集成 EvoSuite 和 Randoop | |
Kex | 用于 Java 字节码的白盒模糊测试工具 | |
EvoSuite DSE | 支持 concolic 测试 |
除了开源框架之外,也有些软件支持自动化单元测试(Automated Unit Testing),包括 Devmate、Diffblue、Ponicode、Symflower 等等。
Randoop
实验
环境
大部分脚本使用 Perl 编写,无法在 Windows 环境中编译
1 | # 下载源码 |
Windows 中可以使用编译好的文件:randoop-4.3.0.zip
1 | # 设置环境变量(powershell) |
默认情况下,Randoop 生成并输出两种单元测试,写入单独的文件。
- 错误显示测试(
error-revealing-tests
)是在执行时失败的测试,表明一个或多个被测类中存在潜在错误。- 当 Randoop 调用创建对象的方法时,Randoop 会验证该对象的格式是否正确。
- 检查:
Object.equals()
、Object.hashCode()
、Object.clone()
、Object.toString()
、Comparable.compareTo()
、Comparator.compare()
、使用@CheckRep
注释的空方法
- 回归测试(
regression-tests
)是在执行时通过的测试,可用于扩充回归测试套件。- 在程序更改/重构之前实施的测试在代码更新后应该仍然成功,否则就可能在代码中引入了新错误。
Randoop 支持对 jar 文件、指定类、指定方法生成 JUnit 测试。
指定多个类
创建 myclasses.txt 文件,指定被测类
1 | java.util.Collections |
调用 randoop 并将执行时间限制为 60 秒
1 | java -classpath $Env:RANDOOP_JAR randoop.main.Main gentests --classlist=myclasses.txt --time-limit=60 |
输出结果,只生成了 RegressionTest,没有 ErrorTest
1 | Randoop for Java version 4.3.0. |
指定类文件
创建文件 Message.java 并编译为 .class 文件
1 | public class Message { |
使用参数 --testclass
指定被测类,注意需要添加 classpath
1 | # 编译 |
生成 199 个回归测试,直接将相关文件都拖到 test 目录下,执行测试,全部通过
创建 Sum.java
1 | public class Sum { |
编译为 .class 文件,并生成测试
1 | # 编译 |
生成 1360 个测试,更改代码
1 | public class Sum { |
重新运行刚才生成的回归测试,1357 个测试失败
指定方法
可能用到的参数
- 指定测试范围的常见度依次递减,同时出错的概率递增:指定 jar,指定类,指定方法
- classlist.txt 示例
- methodlist.txt 示例
参数 | 说明 |
---|---|
--testjar=filename |
将给定 jar 文件中的每个类作为待测类 |
--classlist=filename |
列出要测试的类,所有的方法都将作为被测方法 |
--testclass=string |
指定待测类(完全限定名) |
--methodlist=filename |
测试中调用的方法和构造函数列表(完全限定名) |
--omit-classes=regex |
正则表达式,指定不在测试中使用的类 |
--omit-classes-file=filename |
列出正则表达式,指定不在测试中使用的类 |
--omit-methods=regex |
正则表达式,指定不在测试中使用的方法。不会阻止其他方法对其的间接调用 |
--omit-methods-file=filename |
列出正则表达式,指定不在测试中使用的方法 |
--require-classname-in-test=regex |
正则表达式,指定必须在测试中出现的类。 |
--require-covered-classes=filename |
列出测试必须直接或间接使用的类,包含其中一个即输出 |
--no-regression-tests=boolean |
禁用回归测试输出 |
--deterministic=boolean |
确定性,相同输入相同输出 |
--log=filename |
日志记录 |
--time-limit=int |
0 表示没有限制,如果非零则不确定。总体限制,默认值 100 秒 |
--method-selection=<enum> |
通过从一组被测方法中进行选择来生成新的测试。默认均匀随机(UNIFORM),可选优先考虑分支覆盖率较低的方法(BLOODHOUND) |
--input-selection=<enum> |
通过组合以前生成的旧测试来生成新测试。默认均匀随机(UNIFORM),可选短序列偏好(SMALL_TESTS) |
Randoop 项目想法中包括方法选择策略的优化,其中就提到了让用户指定关键方法,目前似乎还没有具体实现。
-
考虑修改 Randoop 的默认生成器(ForwardGenerator)
- 生成策略
- 满足特定条件时,向标准错误进行输出,应该会存储在 ErrorTest 中
-
使用方法规范(
--specifications=file
)设置特定方法的预期行为,将其归类到错误显示测试中
问题与解决
-
乱码
Settings - Editor -File Encodings
将编码全部改为 UTF-8- 在
idea64.exe.vmoptions
末尾添加-Dfile.encoding=UTF-8
- 重启 IDEA
-
rawtypes
- 泛型类缺少类参数,添加
<?>
即可
- 泛型类缺少类参数,添加
-
程序包不存在
-
cast
相关概念
在 Randoop 中,测试输入表示为 Sequence,测试检查使用 Checks 表示。每个序列由一个或多个 Statement 对象组成。
Randoop 的生成算法创建 Sequence,为每个序列添加适当的 Checks,并将结果作为单元测试输出。
Sequence
Randoop 生成 Sequence 对象,通过添加检查来构造单元测试的输入。在 Randoop 中,所有测试输入都是使用被测类的方法和构造函数的操作序列。
每条语句包含三个元素:
- 正在被调用的特定方法(或构造函数)
- 每次调用返回的值(原始值或对象)
- 调用的输入,来自先前语句中产生的值
1 | Result Operation Inputs |
语句的三个元素映射到 Randoop 中的以下类:
-
Operation 表示语句执行的操作类型
- MethodCall 表示特定的方法调用。该类与 Java 反射 Method 类相似,添加了对 Randoop 有用的附加功能。
- ConstructorCall 表示构造函数调用。
- FieldGet 和 FieldSet 表示获取和设置公共字段。字段由实现 AccessibleField 的类的实例表示。
- EnumConstant 表示一个枚举常量值。
- NonreceiverTerm 表示将变量声明和初始化为原始值、字符串或空值的语句。
- ArrayCreation 表示声明并初始化一个数组的语句,该数组由之前语句中创建的值组成。
-
Variables 代表语句的输入和输出
- Variable 只是序列中索引的包装器:给定序列 s,变量 s.getVariable(i) 表示序列中第 i 个语句产生的值。
创建 sequence
- 通过扩展(extension):获取一个序列并在底部添加一个新语句
- 使用串联(concatenation):给的序列 s1、s2、s3,创建串联的新序列
- Randoop 组合创建新序列的方法
- 从字符串中解析(parseable)出序列
注意:Sequence 是不可变的,扩展操作返回新序列。
如果代码涉及参数化类型,那就需要实例化使用的泛型类类型,以便获得需要的类型替换。
ExecutableSequence
ExecutableSequence 包装 Sequence 并添加两个功能:
- ExecutableSequence 可以通过对预期属性的检查来增强。 Check 是一个对象,表示序列的某些预期属性;例如,序列中的特定方法调用正常返回。当可执行序列被执行时,序列中存在的任何检查都会在运行时检查,并且检查的通过/失败状态可供客户端检查。
- ExecutableSequence 可以执行。 Randoop 使用 Java 的反射机制来调用序列中的方法和构造函数,并使用底层 Sequence 的结构来确定要传递给它们的输入。
执行 sequence
- 在 execute 方法返回后,可以通过 getResult(int i) 方法访问执行过程中创建的运行时对象,该方法返回执行语句 i 的结果。
1 | ExecutableSequence es = new ExecutableSequence(s); |
更多内容参见 Execution 和 ExecutionOutcome 类。
Check
Check 是表示序列的预期属性的对象。就单元测试而言,Check 代表单元测试的一些检查代码。
每个 Check 都与序列中的一个索引相关联,其中该索引表示应该执行检查的时间。
Randoop 通过组合和扩展之前生成的序列来随机生成测试输入(Sequence)。另一方面,Randoop 确定性地执行其检查。对于创建的每个序列,它都会检查该序列的所有对象。
ForwardGenerator
选择器有两个:
inputSequenceSelector
:选择序列operationSelector
:选择方法
1 | /** How to select sequences as input for creating new sequences. */ |
执行序列,在该方法中可以获得序列每条语句的执行结果
- 检查输出的文本信息
1 |
|
生成序列,在该方法中可以选择下一条语句(Operation)并进行拼接
- 默认使用随机均匀的选择器
1 | private ExecutableSequence createNewUniqueSequence() {...} |
Operation 集合从哪里来?
- 通过反射遍历类当中的方法,将访问信息输出到日志中
- reflection 目录下有多种不同的 Extractor,用于提取信息
1 | ForwardGenerator |
EvoSuite
实验
相比 Randoop,EvoSuite 的说明就简陋了很多,但是输出的信息非常多。
EvoSuite 的大量参数都放在了 -D
选项中,需要用键值的方式给出
1 | # 帮助信息 |
报错
1 | ERROR AgentLoader - Exception class java.lang.RuntimeException: AttachProvider.providers() failed to return any provider. Tool classloader: java.net.FactoryURLClassLoader@2c537884 |
扔到 linux 环境下可以成功执行,执行将生成两个文件夹:
- evosuite-tests:生成的单元测试
- evosuite-report:包含 csv 统计数据和 html 报告,例如出参数、测试用例、覆盖率等信息
从输出的测试来看,似乎是对每个类生成测试(而 Randoop 是随机导向测试)
问题
-
和时间相关的参数很多,可配置参数太多
1
2# -D 可选参数
search_budget long 60 Maximum search duration -
统计信息很多,但不一定都用得到
-
从可用参数帮助信息来看,可以最后进行过滤,或直接指定生成测试使用的方法
- 没有说明如果存在依赖会是什么情况
- 注:需要使用字节码签名
1
2
3
4
5-class <arg> target class for test generation. A fully qualifying needs to be provided, e.g. org.foo.SomeClass
# -D 可选参数
junit_strict boolean false Only include test files containing the target classname
target_method String Method for which to generate tests
target_method_list String A colon(:) separated list of methods for which to generate tests -
生成的单元测试依赖 EvoSuite 框架本身,需要添加 evosuite-standalone-runtime.jar
参阅
- randoopTutorial
- Generating JUnit test suites with Randoop
- Java用JUnitテストクラスジェネレータRandoopについて調べてみた
- [Tutorial] An introduction to unit test, regression test and code coverage with IntelliJ
- Randoop Manual
- Randoop Developer’s Manual
- Randoop project ideas
- Commandline | EvoSuite
- Tutorial Part 1: EvoSuite on the Command Line
- class com.sun.tools.attach.AttachNotSupportedException no providers installed #47