自动化单元测试生成:Randoop、EvoSuite与大型语言模型技术对比分析
引言
单元测试作为软件开发质量保障的关键环节,其重要性不言而喻。如何高效、高质量地完成单元测试工作,一直是工程师们关注的焦点。当前自动化测试领域涌现出多种实用的单元测试用例生成技术,本文将重点介绍三种主流方法:基于反馈的随机测试生成工具Randoop、基于进化算法的EvoSuite,以及基于大型语言模型(LLM)的测试生成方法。
一、Randoop技术详解
1.1 技术原理
Randoop是由Carlos Pacheco等人在ICSE 2007会议上提出的著名单元测试自动生成工具[1]。该工具采用反馈导向的随机测试生成策略,专门面向Java代码进行单元测试用例设计。
所谓"反馈导向",是指利用生成用例的执行结果信息来动态调整后续用例的生成策略。这种方法体现了测试设计中的控制思想——通过执行反馈来指导测试生成过程的优化方向。
Randoop的核心生成逻辑包含以下关键步骤:
- 将测试用例定义为一个有序的方法调用序列
- 从待测类的公共方法中随机选择一个目标方法m,同时从已有的成功调用序列集合中随机选取一个序列s作为基础
- 为方法m随机生成输入参数:原始类型参数从有效取值范围内随机选取,对象类型参数则从已生成的对象集合中选取
- 将方法m及其参数添加到序列s的末尾,形成新的调用序列s'
- 执行s'并验证执行结果,若违反预设的契约或规范,则将s'加入失败序列集合;否则加入成功序列集合,并基于s'生成回归测试用例
1.2 使用方法
Randoop的部署和运行相对简单。首先从官方网站下载最新版本安装包并解压,配置环境变量:
set RANDOOP_JAR=D:\cmd_app\randoop-4.3.3\randoop-all-4.3.3.jar将待测类编译完成后,创建包含待测类完全限定名的文本文件,例如:
myclasses.txt
demo.SampleCalculator确保待测类的class文件和环境变量配置正确后,通过以下命令启动测试生成:
java -classpath .;%RANDOOP_JAR% randoop.main.Main gentests --classlist=myclasses.txt --time-limit=60执行完成后,当前目录下将生成两类JUnit测试文件:ErrorTest*.java(缺陷检测测试集)和RegressionTest*.java(回归测试集)。前者用于发现潜在的代码缺陷,后者可作为版本迭代的回归验证用例。
以如下待测类为例:
package demo;
public class SampleCalculator {
public int calculate(int x, int y) {
if (x == 99) {
System.out.println("分支A");
return 777;
} else if (y == 0) {
System.out.println("分支B");
return y;
} else {
System.out.println("分支C");
return x / y;
}
}
public int process(int x, int y) {
int result = this.calculate(x, y);
System.out.println("计算结果:" + result);
return result;
}
}Randoop生成的测试用例示例如下:
@Test
public void testCase001() throws Throwable {
if (debug)
System.out.format("%n%s%n", "RegressionTest1.testCase001");
demo.SampleCalculator calc = new demo.SampleCalculator();
int result1 = calc.calculate((int) '#', (int) (short) 1);
int result2 = calc.calculate((int) (short) -1, (int) (short) 1);
int result3 = calc.process(10, 1);
int result4 = calc.calculate((int) (byte) 0, (-3));
int result5 = calc.process(26, (int) (byte) 1);
int result6 = calc.process((int) 'a', 2);
int result7 = calc.calculate(50, (int) (short) 0);
int result8 = calc.process(1, 1);
java.lang.Class> clazz = calc.getClass();
org.junit.Assert.assertTrue("验证计算结果1", result1 == 35);
org.junit.Assert.assertTrue("验证计算结果2", result2 == (-1));
org.junit.Assert.assertTrue("验证计算结果3", result3 == 10);
org.junit.Assert.assertTrue("验证计算结果4", result4 == 0);
org.junit.Assert.assertTrue("验证计算结果5", result5 == 26);
org.junit.Assert.assertTrue("验证计算结果6", result6 == 48);
org.junit.Assert.assertTrue("验证计算结果7", result7 == 0);
org.junit.Assert.assertTrue("验证计算结果8", result8 == 1);
org.junit.Assert.assertNotNull(clazz);
}二、EvoSuite技术详解
2.1 技术原理
EvoSuite是相对于Randoop更为年轻一代的自动化测试生成工具,于FSE 2011会议上首次亮相[2]。该工具多次在国际测试工具竞赛中获得最高评分,展现了优异的测试生成能力。
EvoSuite的核心设计理念是以测试集的总体覆盖率为优化目标,采用遗传算法进行搜索,并通过变异分析技术控制断言的生成规模。其关键技术要点包括:
- 将最优解定义为一个测试用例集,其中每个用例是被测类的方法调用序列
- 结合聚焦局部搜索和动态符号执行技术,生成方法调用的输入参数
- 采用随机交换策略进行用例集交叉,通过添加新用例或修改现有用例的方式进行变异操作
- 将进化目标适应度定义为与覆盖准则的差距,例如分支覆盖场景下,目标适应度为所有未覆盖分支的距离总和
在断言生成方面,EvoSuite采用了精细化的质量控制策略:
- 分别在原始程序和所有变异体上执行测试步骤,记录可观测的变量值、对象属性等数据
- 分析原始程序与变异体之间的观测差异,为每个差异生成对应的断言
- 从生成的断言集中筛选出能够杀死最多变异体的最小子集
2.2 使用方法
EvoSuite支持多种集成方式,包括命令行工具、Eclipse插件、IntelliJ IDEA插件和Maven插件。本文以Maven插件方式为例进行说明。
首先在项目的pom.xml文件中添加必要的依赖:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.evosuite</groupId>
<artifactId>evosuite-standalone-runtime</artifactId>
<version>1.0.6</version>
<scope>compile</scope>
</dependency>接着配置Maven插件:
<plugin>
<groupId>org.evosuite.plugins</groupId>
<artifactId>evosuite-maven-plugin</artifactId>
<version>1.2.0</version>
</plugin>通过以下命令执行测试生成:
mvn compile evosuite:generate -Dcuts=demo.SampleCalculator evosuite:export -DtargetFolder=src/test/java生成的测试文件将保存在src/test/java目录下,命名为[包名]/[类名]_ESTest.java。示例输出:
package demo;
import org.junit.Test;
import static org.junit.Assert.*;
import demo.SampleCalculator;
import org.evosuite.runtime.EvoRunner;
import org.evosuite.runtime.EvoRunnerParameters;
import org.junit.runner.RunWith;
@RunWith(EvoRunner.class)
@EvoRunnerParameters(mockJVMNonDeterminism = true, useVFS = true, useVNET = true, resetStaticState = true, separateClassLoader = true, useJEE = true)
public class SampleCalculator_ESTest extends SampleCalculator_ESTest_scaffolding {
@Test(timeout = 4000)
public void testCalculate_0() throws Throwable {
SampleCalculator calc = new SampleCalculator();
int result = calc.process(1415, 473);
assertEquals(2, result);
}
@Test(timeout = 4000)
public void testCalculate_1() throws Throwable {
SampleCalculator calc = new SampleCalculator();
int result = calc.calculate(1043, (-592));
assertEquals((-1), result);
}
@Test(timeout = 4000)
public void testCalculate_2() throws Throwable {
SampleCalculator calc = new SampleCalculator();
int result = calc.calculate((-536), 0);
assertEquals(0, result);
}
@Test(timeout = 4000)
public void testCalculate_3() throws Throwable {
SampleCalculator calc = new SampleCalculator();
int result = calc.process(473, 0);
assertEquals(0, result);
}
@Test(timeout = 4000)
public void testCalculate_4() throws Throwable {
SampleCalculator calc = new SampleCalculator();
int result = calc.calculate(0, 0);
assertEquals(777, result);
}
@Test(timeout = 4000)
public void testCalculate_5() throws Throwable {
SampleCalculator calc = new SampleCalculator();
int result = calc.process((-1918), 1);
assertEquals((-1918), result);
}
}三、基于LLM的测试生成技术
3.1 技术原理
近年来,大型语言模型在软件开发领域的应用日益广泛,单元测试用例自动生成也成为了重要的应用场景之一[3]。无论采用何种LLM模型,其背后的基本原理都是相似的——利用经过训练的神经网络模型,将待测代码映射为测试代码。
为提升映射效果,主要有两种优化策略:
策略一:模型微调。通过使用更丰富、更高质量的训练数据,使模型掌握更完整、更合理的代码映射规则。但这种方法面临训练成本过高的挑战。
策略二:提示词工程。通过设计更精确的输入提示,提高映射的准确性。这种方法需要与LLM进行多次交互,效率相对较低。
3.2 使用方法
最直接的使用方式是通过LLM的对话接口提交测试生成任务。以常见的对话式AI为例,针对SampleCalculator类的对话过程如下:
| 用户请求 | 请为以下代码生成JUnit单元测试用例: package demo; public class SampleCalculator { public int calculate(int x, int y) { if(x == 99) { System.out.println("分支A"); return 777; } else if(y == 0) { System.out.println("分支B"); return y; } else{ System.out.println("分支C"); return x / y; } } public int process(int x, int y) { int result = this.calculate(x, y); System.out.println("计算结果:"+ result); return result; } } |
| 生成结果 | 以下是针对SampleCalculator类的JUnit测试用例,覆盖了主要分支和边界条件: package demo; import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Test; public class SampleCalculatorTest { private SampleCalculator calculator; @Before public void setUp() { calculator = new SampleCalculator(); } @Test public void testCalculate_xEquals99() { assertEquals("x等于99时应返回777", 777, calculator.calculate(99, 50)); } @Test public void testCalculate_yEquals0() { assertEquals("y等于0时应返回0", 0, calculator.calculate(50, 0)); } @Test public void testCalculate_normalDivision() { assertEquals("正常除法应返回正确结果", 3, calculator.calculate(15, 5)); } @Test public void testCalculate_negativeNumbers() { assertEquals("负数除法应返回正确结果", 3, calculator.calculate(-15, -5)); } @Test public void testProcess_invokesCalculate() { assertEquals("process应返回calculate的结果", 777, calculator.process(99, 50)); } } |
此外,还可以借助IDE插件工具进行测试生成。例如使用VS Code插件,可以在开发过程中通过交互界面为目标类生成测试用例:
package demo;
import java.util.*;
import java.math.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
public class SampleCalculatorTest {
private MockedStatic<System> mockedSystem;
@BeforeEach
public void setup() {
mockedSystem = mockStatic(System.class);
}
@AfterEach
public void cleanup() {
mockedSystem.close();
}
@Test
public void calculate_WithXEquals99_Returns777() {
SampleCalculator calc = new SampleCalculator();
int result = calc.calculate(99, 1);
assertEquals(777, result);
}
@Test
public void calculate_WithYEquals0_Returns0() {
SampleCalculator calc = new SampleCalculator();
int result = calc.calculate(1, 0);
assertEquals(0, result);
}
@Test
public void calculate_StandardCase_ReturnsDivisionResult() {
SampleCalculator calc = new SampleCalculator();
int result = calc.calculate(20, 4);
assertEquals(5, result);
}
@Test
public void process_PrintsCorrectOutput() {
SampleCalculator calc = new SampleCalculator();
mockedSystem.when(() -> System.out.println("计算结果777")).thenAnswer(i -> null);
int result = calc.process(99, 1);
assertEquals(777, result);
verify(System.out, times(1)).println("计算结果777");
}
}四、技术对比分析
学术界对Randoop和EvoSuite已有较多的对比研究。例如,国内某高校研究团队通过实验发现[4],Randoop生成的测试用例在缺陷检测能力方面表现更优,而EvoSuite生成的测试用例在分支覆盖率指标上更为出色。当然,实验研究存在一定的局限性,具体效果需要结合实际应用场景进行评估。
从技术原理角度分析,EvoSuite在测试生成过程中具有明确的优化策略,对覆盖率和用例规模有较完善的考虑,技术实现较为先进且使用便捷。而Randoop的一个重要优势在于支持自定义契约和规范,能够为测试生成引入需求信息。例如,可以为SampleCalculator类定义如下需求规格:
[
{
"operation":{
"classname":"demo.SampleCalculator",
"name":"process",
"parameterTypes":["int","int"]
},
"identifiers":{
"parameters":["x","y"],
"receiverName":"obj",
"returnName":"ret"
},
"throws":[],
"post":[
{
"property":{
"condition":"ret >= 0",
"description":"返回值应为非负数"
},
"description":"返回非负结果",
"guard":{
"condition":"true",
"description":""
}
}
],
"pre":[]
}
]在生成命令中添加规格文件参数:
java -classpath .;%RANDOOP_JAR% randoop.main.Main gentests --classlist=myclasses.txt --time-limit=60 --specifications=spec.json通过引入需求信息,Randoop可以生成更精准的缺陷检测用例,有效缓解测试预言(oracle)问题[5]。
如果将Randoop和EvoSuite比作擅长逻辑和算法的工科生,那么LLM则更像是擅长理解和表达的文科生。LLM生成的测试用例可读性较强,能够生成较为自然的测试代码描述。然而,这种方法在准确性方面存在明显不足。有研究显示[6],某主流LLM根据被测代码生成的单元测试用例中,能够成功编译的比例约为42.1%,能够正常执行通过的比例仅为24.8%。即使采用前文提及的优化策略,问题仍然难以得到根本改善。这主要由LLM自身的局限性决定——LLM本质上是基于概率的token序列生成器,缺乏对编程规则和语言规范的深层理解能力。例如,它可能不了解只有公有方法和属性才能从类外部访问,也可能不清楚抽象类不能被实例化等基础概念。
五、结论与展望
采用MC/DC等严格测试充分性准则时,单元测试成本可能占据整个开发阶段的50%以上[7]。在项目中合理引入单元测试用例自动生成技术作为质量保障的辅助手段,可以有效减轻研发团队的工作负担。
然而,当前阶段的自动化测试生成技术仍存在多方面局限:用例生成主要依赖随机策略、建立有效断言需要人工介入、生成用例的准确性和可维护性有待提升等。这些问题都需要学术界和工业界持续投入研究,不断改进和完善。
参考文献
- Pacheco C, Lahiri S K, Ernst M D, et al. Feedback-directed random test generation[C]//29th International Conference on Software Engineering (ICSE'07). IEEE, 2007: 75-84.
- Fraser G, Arcuri A. Evosuite: automatic test suite generation for object-oriented software[C]//Proceedings of the 19th ACM SIGSOFT symposium and the 13th European conference on Foundations of software engineering. 2011: 416-419.
- Chen Y, Hu Z, Zhi C, et al. Chatunitest: A framework for llm-based test generation[C]//Companion Proceedings of the 32nd ACM International Conference on the Foundations of Software Engineering. 2024: 572-576.
- 郭丹. Randoop和Evosuite生成测试用例的变异检测能力分析[J]. 现代计算机, 2020(9):6.
- Anand S, Burke E K, Chen T Y, et al. An orchestrated survey of methodologies for automated software test case generation[J]. Journal of Systems & Software, 2013, 86(8):1978-2001.
- Yuan Z, Lou Y, Liu M, et al. No more manual tests? evaluating and improving chatgpt for unit test generation[J]. arXiv preprint arXiv:2305.04207, 2023.
- Hayhurst K J. A practical tutorial on modified condition/decision coverage[M]. DIANE Publishing, 2001.