测试框架

  • 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
2
3
4
5
# 下载源码
git clone https://github.com/randoop/randoop.git

# 编译 Randoop、编译 Java 代理、运行所有测试、构建 jar 文件并从源 Javadoc 更新手册
./gradlew build manual

Windows 中可以使用编译好的文件:randoop-4.3.0.zip

1
2
3
4
5
6
7
8
# 设置环境变量(powershell)
$Env:RANDOOP_JAR = "D:\Study\Github\Java-Deserialization\实验\UnitTest\randoop-all-4.3.0.jar"
$Env:RANDOOP_JAR

# 帮助信息
java -classpath $Env:RANDOOP_JAR randoop.main.Main help gentests

java -classpath randoop-all-4.3.0.jar randoop.main.Main help gentests

默认情况下,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
2
java.util.Collections
java.util.TreeSet

调用 randoop 并将执行时间限制为 60 秒

1
java -classpath $Env:RANDOOP_JAR randoop.main.Main gentests --classlist=myclasses.txt --time-limit=60

输出结果,只生成了 RegressionTest,没有 ErrorTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Randoop for Java version 4.3.0.

Will try to generate tests for 2 classes.
PUBLIC MEMBERS=112
Explorer = ForwardGenerator(steps: 0, null steps: 0, num_sequences_generated: 0;
allSequences: 0, regresson seqs: 0, error seqs: 0=0=0, invalid seqs: 0, subsumed_sequences: 0, num_failed_output_test: 0;
runtimePrimitivesSeen:38)

Progress update: steps=1, test inputs generated=0, failing inputs=0 (2022-02-28T02:36:46.426Z 43.5M used)
Progress update: steps=1000, test inputs generated=823, failing inputs=0 (2022-02-28T02:37:03.368Z 419M used)
Progress update: steps=2000, test inputs generated=1630, failing inputs=0 (2022-02-28T02:37:19.685Z 528M used)
Progress update: steps=3000, test inputs generated=2440, failing inputs=0 (2022-02-28T02:37:35.947Z 339M used)
Progress update: steps=3690, test inputs generated=2960, failing inputs=0 (2022-02-28T02:37:46.426Z 225M used)
Normal method executions: 4962090

Exceptional method executions: 534

Average method execution time (normal termination): 0.000157
Average method execution time (exceptional termination): 0.0369
Approximate memory usage 225M

Explorer = ForwardGenerator(steps: 3690, null steps: 730, num_sequences_generated: 2960;
allSequences: 2960, regresson seqs: 1254, error seqs: 0=0=0, invalid seqs: 0, subsumed_sequences: 0, num_failed_output_test: 1706;
runtimePrimitivesSeen:47)

No error-revealing tests to output.

About to look for failing assertions in 831 regression sequences.

Regression test output:
Regression test count: 831
Writing regression JUnit tests...
Created file D:\Study\Github\randoop\RegressionTest0.java
Created file D:\Study\Github\randoop\RegressionTest1.java
Created file D:\Study\Github\randoop\RegressionTest.java
Wrote regression JUnit tests.
About to look for flaky methods.

Invalid tests generated: 0

Uncompilable sequences generated (count: 1705).
Please report uncompilable sequences at https://github.com/randoop/randoop/issues ,
providing the information requested at https://randoop.github.io/randoop/manual/index.html#bug-reporting .

指定类文件

创建文件 Message.java 并编译为 .class 文件

1
2
3
4
5
6
7
8
9
10
11
public class Message {
private String message;

public Message(String message){
this.message = message;
}
public String printMessage(){
System.out.println(message);
return message;
}
}

使用参数 --testclass 指定被测类,注意需要添加 classpath

1
2
3
4
5
# 编译
javac Message.java

# 生成测试
java -classpath ".;$Env:RANDOOP_JAR" randoop.main.Main gentests --testclass=Message --time-limit=60

生成 199 个回归测试,直接将相关文件都拖到 test 目录下,执行测试,全部通过

创建 Sum.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Sum {
public int add(int a, int b) {
return a + b;
}

public int multiply(int a, int b) {
int result = 0;
for (int i = 0; i < b; i++) {
result = add(result, a);
}
return result;
}
}

编译为 .class 文件,并生成测试

1
2
3
4
5
# 编译
javac Sum.java

# 生成测试
java -classpath ".;$Env:RANDOOP_JAR" randoop.main.Main gentests --testclass=Sum --time-limit=60

生成 1360 个测试,更改代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Sum {
public int add(int a, int b) {
return a + b + 1; // 修改
}

public int multiply(int a, int b) {
int result = 0;
for (int i = 0; i < b; i++) {
result = add(result, a);
}
return result;
}
}

重新运行刚才生成的回归测试,1357 个测试失败

指定方法

可能用到的参数

参数 说明
--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 项目想法中包括方法选择策略的优化,其中就提到了让用户指定关键方法,目前似乎还没有具体实现。

  1. 考虑修改 Randoop 的默认生成器(ForwardGenerator)

    • 生成策略
    • 满足特定条件时,向标准错误进行输出,应该会存储在 ErrorTest 中
  2. 使用方法规范(--specifications=file)设置特定方法的预期行为,将其归类到错误显示测试中

问题与解决

  1. 乱码

    • Settings - Editor -File Encodings 将编码全部改为 UTF-8
    • idea64.exe.vmoptions 末尾添加 -Dfile.encoding=UTF-8
    • 重启 IDEA
  2. rawtypes

    • 泛型类缺少类参数,添加 <?> 即可
  3. 程序包不存在

  4. cast

相关概念

在 Randoop 中,测试输入表示为 Sequence,测试检查使用 Checks 表示。每个序列由一个或多个 Statement 对象组成。

Randoop 的生成算法创建 Sequence,为每个序列添加适当的 Checks,并将结果作为单元测试输出。

Sequence

Randoop 生成 Sequence 对象,通过添加检查来构造单元测试的输入。在 Randoop 中,所有测试输入都是使用被测类的方法和构造函数的操作序列。

每条语句包含三个元素:

  • 正在被调用的特定方法(或构造函数)
  • 每次调用返回的值(原始值或对象)
  • 调用的输入,来自先前语句中产生的值
1
2
3
4
5
6
7
8
                  Result      Operation        Inputs
====== ============= ======
statement 0: l = new LinkedList ()
statement 1: str = "hi!" ()
statement 2: addFirst (l, str)
statement 3: i = size (l)
statement 4: t = new TreeSet (l)
statement 5: s = synchronizedSet (t)

语句的三个元素映射到 Randoop 中的以下类:

  • Operation 表示语句执行的操作类型

    • MethodCall 表示特定的方法调用。该类与 Java 反射 Method 类相似,添加了对 Randoop 有用的附加功能。
    • ConstructorCall 表示构造函数调用。
    • FieldGet 和 FieldSet 表示获取和设置公共字段。字段由实现 AccessibleField 的类的实例表示。
    • EnumConstant 表示一个枚举常量值。
    • NonreceiverTerm 表示将变量声明和初始化为原始值、字符串或空值的语句。
    • ArrayCreation 表示声明并初始化一个数组的语句,该数组由之前语句中创建的值组成。
  • Variables 代表语句的输入和输出

    • Variable 只是序列中索引的包装器:给定序列 s,变量 s.getVariable(i) 表示序列中第 i 个语句产生的值。

创建 sequence

  1. 通过扩展(extension):获取一个序列并在底部添加一个新语句
  2. 使用串联(concatenation):给的序列 s1、s2、s3,创建串联的新序列
    • Randoop 组合创建新序列的方法
  3. 从字符串中解析(parseable)出序列

注意:Sequence 是不可变的,扩展操作返回新序列。

如果代码涉及参数化类型,那就需要实例化使用的泛型类类型,以便获得需要的类型替换。

ExecutableSequence

ExecutableSequence 包装 Sequence 并添加两个功能:

  • ExecutableSequence 可以通过对预期属性的检查来增强。 Check 是一个对象,表示序列的某些预期属性;例如,序列中的特定方法调用正常返回。当可执行序列被执行时,序列中存在的任何检查都会在运行时检查,并且检查的通过/失败状态可供客户端检查。
  • ExecutableSequence 可以执行。 Randoop 使用 Java 的反射机制来调用序列中的方法和构造函数,并使用底层 Sequence 的结构来确定要传递给它们的输入。

执行 sequence

  • 在 execute 方法返回后,可以通过 getResult(int i) 方法访问执行过程中创建的运行时对象,该方法返回执行语句 i 的结果。
1
2
3
4
ExecutableSequence es = new ExecutableSequence(s);
es.execute(null);

ExecutionOutcome resultAt3 = es.getResult(3);

更多内容参见 Execution 和 ExecutionOutcome 类。

Check

Check 是表示序列的预期属性的对象。就单元测试而言,Check 代表单元测试的一些检查代码。

每个 Check 都与序列中的一个索引相关联,其中该索引表示应该执行检查的时间。

Randoop 通过组合和扩展之前生成的序列来随机生成测试输入(Sequence)。另一方面,Randoop 确定性地执行其检查。对于创建的每个序列,它都会检查该序列的所有对象。

ForwardGenerator

选择器有两个:

  • inputSequenceSelector:选择序列
  • operationSelector:选择方法
1
2
3
4
5
/** How to select sequences as input for creating new sequences. */
private final InputSequenceSelector inputSequenceSelector;

/** How to select the method to use for creating a new sequence. */
private final TypedOperationSelector operationSelector;

执行序列,在该方法中可以获得序列每条语句的执行结果

  • 检查输出的文本信息
1
2
@Override
public @Nullable ExecutableSequence step() {...}

生成序列,在该方法中可以选择下一条语句(Operation)并进行拼接

  • 默认使用随机均匀的选择器
1
private ExecutableSequence createNewUniqueSequence() {...}

Operation 集合从哪里来?

  • 通过反射遍历类当中的方法,将访问信息输出到日志中
  • reflection 目录下有多种不同的 Extractor,用于提取信息
1
2
3
4
5
6
ForwardGenerator
GenTests#handle
OperationModel#createModel
OperationModel#addOperationsFromClasses
OperationExtractor#operations
ReflectionManager#apply

EvoSuite

实验

相比 Randoop,EvoSuite 的说明就简陋了很多,但是输出的信息非常多。

EvoSuite 的大量参数都放在了 -D 选项中,需要用键值的方式给出

1
2
3
4
5
6
7
8
# 帮助信息
java -jar evosuite-1.2.0.jar -help

# 可用参数
java -jar evosuite-1.2.0.jar -listParameters

# 生成测试,默认 -generateMOSuite,运行 40min 未完成
java -jar evosuite-1.2.0.jar -target test.jar -generateMOSuite

报错

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. 和时间相关的参数很多,可配置参数太多

    1
    2
    # -D 可选参数
    search_budget long 60 Maximum search duration
  2. 统计信息很多,但不一定都用得到

  3. 从可用参数帮助信息来看,可以最后进行过滤,或直接指定生成测试使用的方法

    • 没有说明如果存在依赖会是什么情况
    • 注:需要使用字节码签名
    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
  4. 生成的单元测试依赖 EvoSuite 框架本身,需要添加 evosuite-standalone-runtime.jar

参阅