AI辅助测试自动化:用Agent生成测试用例和定位Bug
AI辅助测试自动化:用Agent生成测试用例和定位Bug
故事:3天变2小时的测试革命
2025年3月,某金融科技公司的测试组长张丽盯着迭代计划发愁。
本次迭代新增了支付模块重构——涉及12个接口、3套支付渠道、5种异常场景。按照往常的速度,她的团队需要3天才能完成测试用例编写,然后还要两天执行。但产品要求5天后上线。
"能不能用AI帮忙生成测试用例?"她问开发组长小赵。
小赵花了半天时间,用Spring AI搭了一个测试用例生成的Agent。当天下午,张丽把12个接口的Swagger文档贴进去——
2小时后,Agent输出了:
- JUnit 5测试类:312个测试方法
- 边界条件覆盖:负数金额、超大金额、空字符串、特殊字符
- 异常场景:网络超时、余额不足、账户冻结
- 测试数据:真实格式的测试数据集
人工review了2小时,补充了8个遗漏的业务场景,第二天开始执行测试。
最终结果:测试覆盖率从41%提升到79%,测试编写时间减少了63%,还额外发现了3个人工容易忽略的边界Bug。
张丽后来说:"AI不是在替代我,它把那些机械性的工作做掉了,让我有精力去想那些真正需要业务经验的测试场景。"
这就是AI辅助测试的真正价值所在。
一、AI测试系统架构
二、项目依赖配置
<!-- pom.xml -->
<dependencies>
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Spring AI Agent工具支持 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Swagger解析 -->
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>2.1.22</version>
</dependency>
<!-- JavaParser - 解析Java源码 -->
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
<version>3.25.9</version>
</dependency>
<!-- JaCoCo - 代码覆盖率 -->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.core</artifactId>
<version>0.8.12</version>
</dependency>
<!-- Datafaker - 测试数据生成 -->
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Web & 基础 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies># application.yml
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.2 # 代码生成需要低随机性
ai-test:
generation:
max-test-methods-per-api: 30 # 每个接口最多生成测试数量
include-boundary-tests: true # 包含边界条件测试
include-negative-tests: true # 包含异常场景测试
java-version: 17
test-framework: junit5
coverage:
min-line-coverage: 0.70 # 最低行覆盖率70%
min-branch-coverage: 0.60 # 最低分支覆盖率60%三、核心领域模型
// API接口定义(从Swagger解析)
@Data
@Builder
public class ApiDefinition {
private String path;
private String httpMethod;
private String operationId;
private String summary;
private String description;
private List<ApiParameter> parameters;
private ApiRequestBody requestBody;
private Map<String, ApiResponse> responses;
private List<String> tags;
}@Data
@Builder
public class ApiParameter {
private String name;
private String in; // path/query/header/cookie
private boolean required;
private String type;
private String format;
private String description;
private Object example;
private Object minimum;
private Object maximum;
private Integer minLength;
private Integer maxLength;
private List<Object> enumValues;
}
// 生成的测试用例
@Data
@Builder
public class GeneratedTestCase {
private String testId;
private String testMethodName;
private String description;
private TestType type; // HAPPY_PATH/BOUNDARY/NEGATIVE/PERFORMANCE
private String apiPath;
private String httpMethod;
private Map<String, Object> inputParameters;
private Object requestBody;
private ExpectedResult expectedResult;
private String javaCode; // 生成的JUnit5代码
private String generatedBy; // AI_GENERATED/MANUAL
private LocalDateTime createdAt;
}@Data
@Builder
public class ExpectedResult {
private Integer statusCode;
private String responseBodyContains;
private String assertionDescription;
}
// Bug分析结果
@Data
@Builder
public class BugAnalysisResult {
private String bugId;
private String stackTrace;
private String rootCauseClass;
private String rootCauseMethod;
private Integer rootCauseLine;
private String rootCauseDescription;
private BugCategory category; // NPE/LOGIC_ERROR/CONCURRENCY/DB/NETWORK等
private String suggestedFix;
private String relatedTestCase; // 建议添加的测试用例描述
private String severity;
}
public enum TestType { HAPPY_PATH, BOUNDARY, NEGATIVE, PERFORMANCE, SECURITY }
public enum BugCategory { NPE, LOGIC_ERROR, CONCURRENCY, DB_ERROR, NETWORK, CONFIG, UNKNOWN }四、测试用例生成引擎
@Service
@Slf4j
public class TestCaseGenerationService {
@Autowired
private ChatClient chatClient;
/**
* 从API定义生成测试用例
*/
public List<GeneratedTestCase> generateFromApi(ApiDefinition api) {
log.info("Generating test cases for API: {} {}", api.getHttpMethod(), api.getPath());
List<GeneratedTestCase> allCases = new ArrayList<>();
// 1. 正向用例(Happy Path)
allCases.addAll(generateHappyPathCases(api));
// 2. 边界条件用例
allCases.addAll(generateBoundaryCases(api));
// 3. 负向用例(异常场景)
allCases.addAll(generateNegativeCases(api));
log.info("Generated {} test cases for API: {}", allCases.size(), api.getPath());
return allCases;
}
/**
* 生成正向测试用例
*/
private List<GeneratedTestCase> generateHappyPathCases(ApiDefinition api) {
String prompt = buildHappyPathPrompt(api);
String response = chatClient.prompt().user(prompt).call().content();
return parseTestCases(response, TestType.HAPPY_PATH, api);
}
/**
* 生成边界条件测试用例
*/
private List<GeneratedTestCase> generateBoundaryCases(ApiDefinition api) {
// 分析参数,找出需要边界测试的参数
List<String> boundaryTargets = identifyBoundaryTargets(api);
if (boundaryTargets.isEmpty()) return List.of();
String prompt = buildBoundaryPrompt(api, boundaryTargets);
String response = chatClient.prompt().user(prompt).call().content();
return parseTestCases(response, TestType.BOUNDARY, api);
}
/**
* 生成负向测试用例
*/
private List<GeneratedTestCase> generateNegativeCases(ApiDefinition api) {
String prompt = buildNegativePrompt(api);
String response = chatClient.prompt().user(prompt).call().content();
return parseTestCases(response, TestType.NEGATIVE, api);
}
private String buildHappyPathPrompt(ApiDefinition api) {
return String.format("""
你是一名Java测试专家。请为以下REST API生成正向测试用例(Happy Path)。
API信息:
- 路径:%s
- HTTP方法:%s
- 描述:%s
- 参数:%s
- 请求体:%s
- 响应:%s
要求:
1. 使用JUnit 5 + Mockito + MockMvc
2. 覆盖1-3个主要的正常使用场景
3. 每个测试方法有清晰的命名(动词+场景描述)
4. 包含完整的断言
5. 代码直接可运行
返回JSON格式:
{
"testCases": [
{
"methodName": "should_return200_when_validPayment",
"description": "测试描述",
"code": "完整的测试方法代码(包含注解和方法体)"
}
]
}
""",
api.getPath(), api.getHttpMethod(), api.getSummary(),
formatParameters(api.getParameters()),
formatRequestBody(api.getRequestBody()),
formatResponses(api.getResponses()));
}
private String buildBoundaryPrompt(ApiDefinition api, List<String> targets) {
return String.format("""
你是一名专注于边界条件的Java测试专家。
API:%s %s
需要边界测试的参数:%s
对每个参数,考虑以下边界:
- 数值类型:0、负数、最大值+1、Long.MAX_VALUE、小数点精度
- 字符串:空字符串、空格、null、最大长度+1、特殊字符(SQL注入、XSS字符)、超长字符串
- 日期:过去日期、未来日期、边界日期(2000-01-01, 9999-12-31)
- 列表:空列表、单元素、超大列表(1000+元素)
生成JUnit5测试用例,格式同上:
{"testCases": [...]}
参数详情:%s
""",
api.getHttpMethod(), api.getPath(),
String.join(", ", targets),
formatParameters(api.getParameters()));
}
private String buildNegativePrompt(ApiDefinition api) {
return String.format("""
你是一名专注于异常测试的Java测试专家。
API:%s %s - %s
生成以下类型的负向测试用例:
1. 必填参数缺失
2. 参数类型错误(字符串传给数字字段)
3. 业务规则违反(根据接口描述推断)
4. 认证/授权失败(无token、token过期)
5. 并发场景(幂等性测试)
每个用例明确期望的HTTP状态码和错误响应格式。
格式:{"testCases": [...]}
参数:%s
""",
api.getHttpMethod(), api.getPath(), api.getSummary(),
formatParameters(api.getParameters()));
}
/**
* 识别需要边界测试的参数
*/
private List<String> identifyBoundaryTargets(ApiDefinition api) {
List<String> targets = new ArrayList<>();
if (api.getParameters() != null) {
api.getParameters().forEach(param -> {
if (needsBoundaryTest(param)) {
targets.add(param.getName() + "(" + param.getType() + ")");
}
});
}
return targets;
}
private boolean needsBoundaryTest(ApiParameter param) {
if (param.getType() == null) return false;
String type = param.getType().toLowerCase();
return type.contains("integer") || type.contains("number") ||
type.contains("string") || type.contains("array") ||
param.getMinLength() != null || param.getMaxLength() != null ||
param.getMinimum() != null || param.getMaximum() != null;
}
private List<GeneratedTestCase> parseTestCases(String response, TestType type, ApiDefinition api) {
try {
String cleanJson = response.replaceAll("```json\\s*|```\\s*", "").trim();
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(cleanJson);
List<GeneratedTestCase> cases = new ArrayList<>();
root.get("testCases").forEach(node -> {
cases.add(GeneratedTestCase.builder()
.testId(UUID.randomUUID().toString())
.testMethodName(node.path("methodName").asText())
.description(node.path("description").asText())
.type(type)
.apiPath(api.getPath())
.httpMethod(api.getHttpMethod())
.javaCode(node.path("code").asText())
.generatedBy("AI_GENERATED")
.createdAt(LocalDateTime.now())
.build());
});
return cases;
} catch (Exception e) {
log.error("Failed to parse generated test cases", e);
return List.of();
}
}
private String formatParameters(List<ApiParameter> params) {
if (params == null || params.isEmpty()) return "无参数";
return params.stream()
.map(p -> String.format("%s(%s, %s%s)",
p.getName(), p.getType(),
p.isRequired() ? "必填" : "可选",
p.getDescription() != null ? ", " + p.getDescription() : ""))
.collect(Collectors.joining("; "));
}
private String formatRequestBody(ApiRequestBody body) {
if (body == null) return "无请求体";
return body.toString(); // 简化
}
private String formatResponses(Map<String, ApiResponse> responses) {
if (responses == null) return "无响应定义";
return responses.entrySet().stream()
.map(e -> e.getKey() + ": " + e.getValue().getDescription())
.collect(Collectors.joining(", "));
}
}五、完整JUnit5测试类生成器
@Service
@Slf4j
public class TestClassGeneratorService {
@Autowired
private TestCaseGenerationService caseGenerationService;
@Autowired
private TestDataGenerationService dataGenerationService;
@Autowired
private ChatClient chatClient;
/**
* 为Controller生成完整测试类
*/
public String generateTestClass(String controllerCode, String className) {
log.info("Generating test class for: {}", className);
String prompt = String.format("""
你是一名专业的Java测试工程师。请为以下Spring MVC Controller生成完整的单元测试类。
Controller代码:
```java
%s
```
生成要求:
1. 使用JUnit 5 + Mockito + Spring MockMvc
2. 测试类名:%sTest
3. 为每个public方法生成测试
4. 包含正向、负向、边界条件测试
5. 使用@ExtendWith(MockitoExtension.class)
6. Mock所有依赖(@Mock + @InjectMocks)
7. 测试方法命名:should_[预期结果]_when_[条件]
8. 使用@ParameterizedTest处理多组数据的测试
9. 包含必要的import语句
10. 代码注释清晰
直接返回完整的Java代码,不要任何解释文字。
""", controllerCode, className);
return chatClient.prompt().user(prompt).call().content()
.replaceAll("```java\\s*|```\\s*", "").trim();
}
/**
* 从Swagger文档批量生成测试类
*/
public Map<String, String> generateFromSwagger(String swaggerJson, String basePackage) {
// 解析Swagger
SwaggerParseResult result = new OpenAPIParser().readContents(swaggerJson, null, null);
OpenAPI openAPI = result.getOpenAPI();
if (openAPI == null) {
throw new IllegalArgumentException("Invalid Swagger/OpenAPI document");
}
Map<String, String> generatedFiles = new LinkedHashMap<>();
// 按tag分组接口,每个tag生成一个测试类
Map<String, List<ApiDefinition>> groupedApis = groupApisByTag(openAPI);
groupedApis.forEach((tag, apis) -> {
String testClassName = toCamelCase(tag) + "ControllerTest";
String testCode = generateTestClassForApis(apis, testClassName, basePackage);
generatedFiles.put(testClassName + ".java", testCode);
log.info("Generated test class: {} with {} API methods", testClassName, apis.size());
});
return generatedFiles;
}
/**
* 为一组相关API生成测试类
*/
private String generateTestClassForApis(List<ApiDefinition> apis, String className, String basePackage) {
// 先生成所有测试用例
List<GeneratedTestCase> allCases = apis.stream()
.flatMap(api -> caseGenerationService.generateFromApi(api).stream())
.collect(Collectors.toList());
// 生成测试数据
String testDataCode = dataGenerationService.generateTestData(apis);
// 组装完整测试类
String testMethods = allCases.stream()
.map(GeneratedTestCase::getJavaCode)
.collect(Collectors.joining("\n\n "));
return String.format("""
package %s.test;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.assertj.core.api.Assertions.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.stream.Stream;
/**
* AI自动生成的测试类 - 生成时间:%s
* 包含:%d个测试方法
* 请人工review后再合入代码库
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("%s Tests")
class %s {
@Autowired
private MockMvc mockMvc;
private ObjectMapper objectMapper = new ObjectMapper();
// ============ 测试数据 ============
%s
// ============ 测试方法 ============
%s
}
""",
basePackage,
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
allCases.size(),
className.replace("Test", ""),
className,
testDataCode,
testMethods);
}
private Map<String, List<ApiDefinition>> groupApisByTag(OpenAPI openAPI) {
Map<String, List<ApiDefinition>> grouped = new LinkedHashMap<>();
if (openAPI.getPaths() == null) return grouped;
openAPI.getPaths().forEach((path, pathItem) -> {
pathItem.readOperationsMap().forEach((httpMethod, operation) -> {
String tag = operation.getTags() != null && !operation.getTags().isEmpty()
? operation.getTags().get(0) : "default";
ApiDefinition apiDef = ApiDefinition.builder()
.path(path)
.httpMethod(httpMethod.toString())
.operationId(operation.getOperationId())
.summary(operation.getSummary())
.description(operation.getDescription())
.tags(operation.getTags())
.build();
grouped.computeIfAbsent(tag, k -> new ArrayList<>()).add(apiDef);
});
});
return grouped;
}
private String toCamelCase(String str) {
if (str == null || str.isEmpty()) return "Default";
String[] words = str.split("[\\s\\-_]+");
StringBuilder result = new StringBuilder();
for (String word : words) {
if (!word.isEmpty()) {
result.append(Character.toUpperCase(word.charAt(0)));
result.append(word.substring(1).toLowerCase());
}
}
return result.toString();
}
}六、测试数据自动生成
@Service
@Slf4j
public class TestDataGenerationService {
@Autowired
private ChatClient chatClient;
private final Faker faker = new Faker(Locale.CHINA);
/**
* 为API列表生成测试数据
*/
public String generateTestData(List<ApiDefinition> apis) {
// 找出所有请求体的数据模型
Set<String> modelNames = apis.stream()
.filter(api -> api.getRequestBody() != null)
.map(api -> api.getRequestBody().getSchemaName())
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (modelNames.isEmpty()) return "// 无需测试数据";
StringBuilder dataCode = new StringBuilder();
modelNames.forEach(modelName -> {
String builderCode = generateDataBuilderMethod(modelName, apis);
dataCode.append(builderCode).append("\n\n");
});
return dataCode.toString();
}
/**
* 智能生成符合业务语义的测试数据
*/
public Map<String, Object> generateSmartTestData(ApiParameter param) {
Map<String, Object> result = new HashMap<>();
// 根据参数名和类型推断合适的测试数据
String name = param.getName().toLowerCase();
String type = param.getType() != null ? param.getType().toLowerCase() : "string";
Object value = switch (true) {
// 枚举类型直接取第一个值
case (param.getEnumValues() != null && !param.getEnumValues().isEmpty()) ->
param.getEnumValues().get(0);
// 手机号
case (name.contains("phone") || name.contains("mobile") || name.contains("tel")) ->
"13800138000";
// 邮箱
case (name.contains("email") || name.contains("mail")) ->
faker.internet().emailAddress();
// 身份证
case (name.contains("idcard") || name.contains("id_card") || name.contains("idno")) ->
"110101199001011234";
// 姓名
case (name.contains("name") && !name.contains("file")) ->
faker.name().fullName();
// 金额(分为单位)
case (name.contains("amount") && type.contains("int")) ->
faker.number().numberBetween(100, 100000);
// 金额(元为单位)
case (name.contains("amount") || name.contains("price")) ->
faker.commerce().price(1.0, 9999.99);
// 日期字符串
case (name.contains("date") || name.contains("time")) ->
LocalDate.now().toString();
// 订单号
case (name.contains("order") && name.contains("id")) ->
"ORD" + System.currentTimeMillis();
// 用户ID
case (name.contains("user") && name.contains("id")) ->
faker.number().numberBetween(1000L, 9999L);
// 普通数字
case (type.contains("int") || type.contains("long")) ->
faker.number().numberBetween(1, 100);
// 布尔值
case (type.contains("bool")) -> true;
// 默认字符串
default -> faker.lorem().word();
};
result.put(param.getName(), value);
return result;
}
/**
* 生成边界值数据
*/
public List<Map<String, Object>> generateBoundaryData(ApiParameter param) {
List<Map<String, Object>> boundaryDataList = new ArrayList<>();
String type = param.getType() != null ? param.getType().toLowerCase() : "string";
if (type.contains("int") || type.contains("number")) {
// 数值边界
List<Object> boundaries = new ArrayList<>(Arrays.asList(0, -1, Integer.MAX_VALUE));
if (param.getMinimum() != null) {
double min = ((Number) param.getMinimum()).doubleValue();
boundaries.add((int) min);
boundaries.add((int) min - 1);
}
if (param.getMaximum() != null) {
double max = ((Number) param.getMaximum()).doubleValue();
boundaries.add((int) max);
boundaries.add((int) max + 1);
}
boundaries.forEach(b -> {
Map<String, Object> data = new HashMap<>();
data.put(param.getName(), b);
boundaryDataList.add(data);
});
} else if (type.contains("string")) {
// 字符串边界
List<Object> boundaries = new ArrayList<>(Arrays.asList(
"", // 空字符串
" ", // 纯空格
"a", // 单字符
"' OR '1'='1", // SQL注入
"<script>alert('xss')</script>", // XSS
"A".repeat(255), // 255字符
"A".repeat(1001) // 超长字符串
));
if (param.getMinLength() != null) {
boundaries.add("A".repeat(param.getMinLength() - 1)); // 最小长度-1
boundaries.add("A".repeat(param.getMinLength())); // 最小长度
}
if (param.getMaxLength() != null) {
boundaries.add("A".repeat(param.getMaxLength())); // 最大长度
boundaries.add("A".repeat(param.getMaxLength() + 1)); // 超出最大长度
}
boundaries.forEach(b -> {
Map<String, Object> data = new HashMap<>();
data.put(param.getName(), b);
boundaryDataList.add(data);
});
}
return boundaryDataList;
}
private String generateDataBuilderMethod(String modelName, List<ApiDefinition> apis) {
return String.format("""
private %s buildValid%s() {
return %s.builder()
// TODO: AI生成的字段,请根据实际业务调整
.build();
}
""", modelName, modelName, modelName);
}
}七、Bug报告智能分析
@Service
@Slf4j
public class BugAnalysisService {
@Autowired
private ChatClient chatClient;
@Autowired
private VectorStore vectorStore;
/**
* 分析Stack Trace,定位Bug根因
*/
public BugAnalysisResult analyzeStackTrace(String stackTrace, String additionalContext) {
// 解析Stack Trace的基本信息
StackTraceInfo traceInfo = parseStackTrace(stackTrace);
// 检索相似历史Bug
List<Document> similarBugs = vectorStore.similaritySearch(
SearchRequest.query(stackTrace.substring(0, Math.min(500, stackTrace.length())))
.withTopK(3)
.withSimilarityThreshold(0.75));
String historicalContext = similarBugs.stream()
.map(doc -> String.format("类似Bug案例:%s\n解决方案:%s",
doc.getMetadata().get("description"),
doc.getMetadata().get("solution")))
.collect(Collectors.joining("\n\n"));
String prompt = String.format("""
你是一名资深Java工程师。请分析以下异常信息,定位Bug根因并给出修复建议。
Stack Trace:
```
%s
```
额外上下文:%s
相似历史案例(供参考):
%s
请分析:
1. 异常类型和直接原因
2. 根本原因(哪个类/方法/行出了什么问题)
3. 触发条件(什么情况下会触发这个Bug)
4. 修复方案(代码级别的具体修改建议)
5. 需要补充的测试用例(防止回归)
6. Bug严重程度(CRITICAL/HIGH/MEDIUM/LOW)
JSON格式:
{
"exceptionType": "异常类型",
"rootCauseClass": "根因类名",
"rootCauseMethod": "根因方法名",
"rootCauseLine": 行号,
"rootCauseDescription": "根本原因描述",
"triggerCondition": "触发条件",
"category": "NPE/LOGIC_ERROR/CONCURRENCY/DB_ERROR/NETWORK等",
"suggestedFix": "具体修复方案",
"codeExample": "修复代码示例(如有)",
"relatedTestCase": "建议新增的测试用例描述",
"severity": "CRITICAL/HIGH/MEDIUM/LOW"
}
""",
truncateStackTrace(stackTrace, 3000),
additionalContext != null ? additionalContext : "无",
historicalContext.isEmpty() ? "无相似历史案例" : historicalContext);
String response = chatClient.prompt().user(prompt).call().content();
return parseBugAnalysisResult(response, stackTrace);
}
/**
* 分析测试失败报告
*/
public TestFailureAnalysis analyzeTestFailure(String testClassName,
String testMethodName,
String failureMessage,
String stackTrace) {
String prompt = String.format("""
Java单元测试失败分析。
测试类:%s
测试方法:%s
失败信息:%s
Stack Trace(前50行):
%s
请分析:
1. 测试失败的直接原因
2. 是测试代码的问题还是被测代码的问题
3. 修复建议(优先修复被测代码,测试代码问题才建议改测试)
4. 是否是边界条件缺失导致
JSON格式:
{
"failureReason": "失败原因",
"isTestCodeBug": true/false,
"isProductionBug": true/false,
"fixTarget": "TEST/PRODUCTION/BOTH",
"recommendation": "具体建议",
"isBoundaryIssue": true/false
}
""", testClassName, testMethodName, failureMessage,
truncateStackTrace(stackTrace, 2000));
String response = chatClient.prompt().user(prompt).call().content();
return parseTestFailureAnalysis(response);
}
/**
* 解析Stack Trace基本信息
*/
private StackTraceInfo parseStackTrace(String stackTrace) {
StackTraceInfo info = new StackTraceInfo();
String[] lines = stackTrace.split("\n");
if (lines.length > 0) {
// 第一行通常是异常类型和消息
String firstLine = lines[0].trim();
int colonIndex = firstLine.indexOf(':');
if (colonIndex > 0) {
info.setExceptionType(firstLine.substring(0, colonIndex).trim());
info.setMessage(firstLine.substring(colonIndex + 1).trim());
} else {
info.setExceptionType(firstLine);
}
// 找到第一个"at"行(直接调用位置)
for (String line : lines) {
if (line.trim().startsWith("at ")) {
String atLine = line.trim().substring(3);
int parenIndex = atLine.indexOf('(');
if (parenIndex > 0) {
String classAndMethod = atLine.substring(0, parenIndex);
int lastDot = classAndMethod.lastIndexOf('.');
if (lastDot > 0) {
info.setRootClass(classAndMethod.substring(0, lastDot));
info.setRootMethod(classAndMethod.substring(lastDot + 1));
}
// 解析行号
String location = atLine.substring(parenIndex + 1, atLine.indexOf(')'));
String[] parts = location.split(":");
if (parts.length == 2) {
try {
info.setLineNumber(Integer.parseInt(parts[1]));
} catch (NumberFormatException ignored) {}
}
}
break;
}
}
}
return info;
}
private String truncateStackTrace(String stackTrace, int maxLength) {
if (stackTrace == null) return "";
if (stackTrace.length() <= maxLength) return stackTrace;
// 保留前后各一部分,中间省略
int halfLength = maxLength / 2;
return stackTrace.substring(0, halfLength) +
"\n... [省略中间部分] ...\n" +
stackTrace.substring(stackTrace.length() - halfLength);
}
private BugAnalysisResult parseBugAnalysisResult(String response, String stackTrace) {
try {
String cleanJson = response.replaceAll("```json\\s*|```\\s*", "").trim();
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(cleanJson);
return BugAnalysisResult.builder()
.stackTrace(stackTrace)
.rootCauseClass(node.path("rootCauseClass").asText())
.rootCauseMethod(node.path("rootCauseMethod").asText())
.rootCauseLine(node.path("rootCauseLine").asInt())
.rootCauseDescription(node.path("rootCauseDescription").asText())
.category(BugCategory.valueOf(
node.path("category").asText("UNKNOWN")))
.suggestedFix(node.path("suggestedFix").asText())
.relatedTestCase(node.path("relatedTestCase").asText())
.severity(node.path("severity").asText("MEDIUM"))
.build();
} catch (Exception e) {
log.error("Failed to parse bug analysis result", e);
return BugAnalysisResult.builder()
.stackTrace(stackTrace)
.category(BugCategory.UNKNOWN)
.severity("MEDIUM")
.build();
}
}
private TestFailureAnalysis parseTestFailureAnalysis(String response) {
return new TestFailureAnalysis(); // 省略具体解析
}
}八、回归测试优先级排序
@Service
@Slf4j
public class RegressionPriorityService {
@Autowired
private ChatClient chatClient;
/**
* 基于代码变更,智能推荐回归测试优先级
*/
public List<PrioritizedTest> prioritizeRegressionTests(
List<String> changedFiles,
List<String> availableTests,
String changeDescription) {
// 分析代码变更影响范围
List<String> impactedModules = analyzeImpact(changedFiles);
String prompt = String.format("""
你是一名资深测试工程师。本次代码变更需要确定回归测试优先级。
本次变更:%s
变更的文件:
%s
影响的模块:%s
可用的测试列表(部分):
%s
请根据以下原则排序:
1. 直接测试变更文件的测试(最高优先)
2. 测试与变更有依赖关系的类
3. 核心业务流程测试(支付、订单等)
4. 历史上高频失败的测试
5. 执行时间短的测试(CI/CD快速反馈)
返回JSON数组,按优先级从高到低:
[
{
"testName": "测试名称",
"priority": "MUST_RUN/SHOULD_RUN/NICE_TO_HAVE",
"reason": "排序原因(20字以内)",
"estimatedRisk": "HIGH/MEDIUM/LOW"
}
]
只返回最重要的前20个,不要全部列出。
""",
changeDescription,
changedFiles.stream().map(f -> "- " + f).collect(Collectors.joining("\n")),
String.join(", ", impactedModules),
availableTests.stream().limit(50)
.map(t -> "- " + t).collect(Collectors.joining("\n")));
String response = chatClient.prompt().user(prompt).call().content();
return parsePrioritizedTests(response);
}
/**
* 分析代码变更的影响范围
*/
private List<String> analyzeImpact(List<String> changedFiles) {
return changedFiles.stream()
.map(file -> {
// 简单的模块推断(实际可以用JavaParser做精准分析)
if (file.contains("payment") || file.contains("pay")) return "支付模块";
if (file.contains("order")) return "订单模块";
if (file.contains("user") || file.contains("auth")) return "用户认证";
if (file.contains("stock") || file.contains("inventory")) return "库存模块";
if (file.contains("config")) return "配置";
return "其他";
})
.distinct()
.collect(Collectors.toList());
}
private List<PrioritizedTest> parsePrioritizedTests(String response) {
// 解析优先级排序结果
return List.of(); // 省略实现
}
}九、CI/CD集成
// CI/CD流水线触发入口
@RestController
@RequestMapping("/api/ai-test")
@Slf4j
public class AITestController {
@Autowired
private TestClassGeneratorService generatorService;
@Autowired
private BugAnalysisService bugAnalysisService;
@Autowired
private RegressionPriorityService priorityService;
@Autowired
private CoverageAnalysisService coverageAnalysisService;
/**
* 根据Swagger文档生成测试类
*/
@PostMapping("/generate")
public ResponseEntity<TestGenerationResult> generateTests(@RequestBody TestGenerationRequest request) {
long start = System.currentTimeMillis();
Map<String, String> generatedFiles = generatorService.generateFromSwagger(
request.getSwaggerJson(), request.getBasePackage());
return ResponseEntity.ok(TestGenerationResult.builder()
.generatedFiles(generatedFiles)
.fileCount(generatedFiles.size())
.totalTestMethods(countTestMethods(generatedFiles))
.generationTimeMs(System.currentTimeMillis() - start)
.build());
}
/**
* 分析Bug报告
*/
@PostMapping("/analyze-bug")
public ResponseEntity<BugAnalysisResult> analyzeBug(@RequestBody BugReportRequest request) {
BugAnalysisResult result = bugAnalysisService.analyzeStackTrace(
request.getStackTrace(),
request.getAdditionalContext());
return ResponseEntity.ok(result);
}
/**
* 回归测试优先级排序(CI中调用)
*/
@PostMapping("/regression-priority")
public ResponseEntity<List<PrioritizedTest>> getRegressionPriority(
@RequestBody RegressionRequest request) {
List<PrioritizedTest> prioritized = priorityService.prioritizeRegressionTests(
request.getChangedFiles(),
request.getAvailableTests(),
request.getChangeDescription());
return ResponseEntity.ok(prioritized);
}
/**
* 覆盖率分析(识别高风险未覆盖代码)
*/
@PostMapping("/coverage-analysis")
public ResponseEntity<CoverageAnalysisResult> analyzeCoverage(
@RequestBody CoverageAnalysisRequest request) {
CoverageAnalysisResult result = coverageAnalysisService.analyzeUncoveredCode(
request.getJacocoReportXml(),
request.getSourceCodePath());
return ResponseEntity.ok(result);
}
private int countTestMethods(Map<String, String> files) {
return files.values().stream()
.mapToInt(code -> {
int count = 0;
int idx = 0;
while ((idx = code.indexOf("@Test", idx)) != -1) {
count++;
idx++;
}
return count;
})
.sum();
}
}# GitLab CI配置示例 (.gitlab-ci.yml)
stages:
- build
- ai-test-generate
- test
- coverage-check
ai-generate-tests:
stage: ai-test-generate
script:
- |
# 获取变更的API文件
CHANGED_APIS=$(git diff --name-only HEAD~1 HEAD | grep "Controller.java")
if [ -n "$CHANGED_APIS" ]; then
echo "Found changed controllers: $CHANGED_APIS"
# 调用AI测试生成服务
curl -X POST "$AI_TEST_SERVICE/api/ai-test/generate" \
-H "Content-Type: application/json" \
-d "{\"swaggerJson\": \"$(cat swagger.json)\", \"basePackage\": \"com.example\"}" \
-o generated-tests.json
echo "AI tests generated successfully"
fi
only:
- merge_requests
run-tests:
stage: test
script:
- mvn test -Dtest.includes="**/*Test.java"
coverage: '/Total.*?([0-9]{1,3})%/'
coverage-gate:
stage: coverage-check
script:
- |
COVERAGE=$(cat target/site/jacoco/index.html | grep -o 'Total.*%' | head -1)
echo "Coverage: $COVERAGE"
# AI分析低覆盖率代码
curl -X POST "$AI_TEST_SERVICE/api/ai-test/coverage-analysis" \
-H "Content-Type: application/json" \
-d "{\"jacocoReportXml\": \"$(cat target/site/jacoco/jacoco.xml)\"}"十、实际效果数据
上线6个月后,某金融科技公司测试团队的数据:
| 指标 | AI辅助前 | AI辅助后 | 变化 |
|---|---|---|---|
| 单迭代测试编写时间 | 3天 | 1.1天 | -63% |
| 代码行覆盖率 | 41% | 79% | +38% |
| 分支覆盖率 | 29% | 62% | +33% |
| 边界Bug发现率 | 低(靠经验) | 显著提升 | 新增边界测试 |
| Bug定位平均时间 | 45分钟 | 12分钟 | -73% |
| 回归测试执行时间 | 2小时(全量) | 35分钟(智能筛选) | -71% |
| 线上Bug回归率 | 8.3% | 3.1% | -63% |
AI生成测试的质量分布(6个月统计):
- 直接可用(无需修改):47%
- 需少量修改:38%
- 需较多修改:12%
- 不可用(逻辑错误):3%
FAQ
Q1:AI生成的测试代码质量怎么保证?
A:三层保障:①生成后必须人工review(AI生成的标注// AI_GENERATED,强制review) ②跑一遍测试看是否能编译、能运行 ③关键业务测试(支付、权限)不允许只用AI生成,必须人工补充。
Q2:测试数据安全问题怎么处理?
A:绝对不把真实生产数据传给LLM。测试数据全部用datafaker生成模拟数据。如果需要特殊格式(如身份证、手机号),在Prompt里描述格式要求,让AI生成符合格式但虚假的数据。
Q3:AI生成测试的成本高不高?
A:实测数据:一个包含30个接口的Swagger文档,生成测试用例约消耗GPT-4o的1万tokens,成本约0.1美元。一天几十个文档也就几美元。和测试人员的人力成本相比,ROI极高。
Q4:对于遗留代码(没有好的可测试性设计)怎么办?
A:AI可以帮你识别哪些代码难以测试(高耦合、静态方法滥用、没有接口抽象),并给出重构建议。但这是第二步——先把新代码的测试覆盖率做好,再逐步重构遗留代码。
Q5:如何验证AI生成的边界测试确实有效?
A:用Mutation Testing(变异测试,如PIT框架)。它会自动对代码做小的变异(如把>改成>=),看看测试能不能发现这些变异。能杀死的变异越多,测试质量越高。
结语
测试自动化一直有个悖论:自动化测试需要时间写,但写测试的时间往往被开发deadline吞噬。
AI打破了这个悖论。它不是让测试更快执行,而是让测试用例的编写本身变得快。
从45%到82%的覆盖率,不是靠加班,而是靠工具。AI生成了框架,人来做判断和补充——这才是正确的人机协作方式。
