Files
wenzi/AI_TESTING_QUICK_FIX_GUIDE.md

569 lines
15 KiB
Markdown
Raw Normal View History

# AI测试常见问题速查表
基于蚊子项目1210+测试经验
快速识别和修复AI生成测试的常见问题
---
## 🚨 第一类:虚假测试(立即修复)
### 1.1 测试框架本身
**错误示例**AI常生成
```java
@Test
void shouldReturnName_whenGetName() {
User user = new User("John");
assertEquals("John", user.getName()); // 测试了Lombok
}
@Test
void shouldSetAge_whenSetAge() {
User user = new User();
user.setAge(25);
assertEquals(25, user.getAge()); // 测试了setter
}
```
**正确做法**
```java
// 不测试getter/setter除非有自定义逻辑
// 删除或合并为参数化测试
@ParameterizedTest
@CsvSource({
"John, 25, active",
"Jane, 30, inactive"
})
void shouldCreateUser_withValidData(String name, int age, String status) {
User user = User.builder()
.name(name)
.age(age)
.status(status)
.build();
assertThat(user.getName()).isEqualTo(name);
assertThat(user.getAge()).isEqualTo(age);
assertThat(user.getStatus()).isEqualTo(status);
}
```
**检查方法**
```bash
# 统计getter/setter测试数量
grep -r "void should.*when[Gg]et\|void should.*when[Ss]et" src/test/java | wc -l
# 如果>20个需要清理
```
---
### 1.2 虚假断言
**错误示例**(无意义断言)
```java
@Test
void shouldProcessOrder() {
Order result = orderService.process(orderRequest);
assertNotNull(result); // 太弱
assertNotNull(result.getId()); // 还是太弱
assertTrue(result.getTotal() > 0); // 不够具体
// 缺少:验证具体金额、状态、库存扣减
}
@Test
void shouldValidateInput() {
// 没有实际验证
assertTrue(true); // 总是通过!
}
```
**正确做法**
```java
@Test
void shouldCalculateTotalCorrectly_whenProcessingOrder() {
// Given
OrderRequest request = createOrderRequest(BigDecimal.valueOf(100), 2);
// When
Order result = orderService.process(request);
// Then - 验证具体业务结果
assertThat(result.getTotal())
.isEqualByComparingTo(BigDecimal.valueOf(200)); // 精确验证
assertThat(result.getStatus()).isEqualTo("PAID");
assertThat(result.getItems()).hasSize(2);
// 验证副作用
verify(inventoryService).deductStock("SKU001", 2);
verify(paymentService).charge(eq("USER001"), eq(BigDecimal.valueOf(200)));
// 验证数据库状态
Order saved = orderRepository.findById(result.getId()).orElseThrow();
assertThat(saved.getCreatedAt()).isNotNull();
}
```
**识别脚本**
```bash
# 查找虚假断言
find src/test/java -name "*Test.java" -exec grep -l "assertTrue(true)\|assertNotNull(new" {} \;
# 统计assertNotNull使用率
find src/test/java -name "*Test.java" -exec grep -c "assertNotNull" {} \; | sort -rn | head -20
```
---
## 🎯 第二类:边界条件缺失(必须补充)
### 2.1 系统化边界矩阵
| 类型 | 必须测试的值 | AI常遗漏 |
|------|------------|---------|
| **数值** | MIN_VALUE, -1, 0, 1, MAX_VALUE | 极大/极小值 |
| **字符串** | null, "", "a", "最大长度", "特殊字符🔑" | 超长、Unicode |
| **集合** | null, empty, 1 element, max size | null、空列表 |
| **时间** | MIN, epoch, now, MAX, null | 边界时间 |
| **并发** | 1 thread, 10 threads, race condition | 并发测试 |
**错误示例**(缺少边界)
```java
@Test
void shouldCalculateDiscount() {
BigDecimal price = BigDecimal.valueOf(100);
String userType = "VIP";
BigDecimal discount = calculator.calculate(price, userType);
assertEquals(BigDecimal.valueOf(80), discount); // 只测试正常值
}
```
**正确做法**(参数化边界测试)
```java
@ParameterizedTest
@CsvSource({
"100, VIP, 80", // 正常VIP
"100, NORMAL, 100", // 正常普通用户
"0, VIP, 0", // 边界0价格
"-10, VIP, -8", // 边界:负数价格
"100, null, 100", // 边界null用户类型
"100, UNKNOWN, 100", // 边界:未知类型
"999999999, VIP, 799999999" // 边界:极大值
})
void shouldCalculateDiscount_withBoundaryValues(
BigDecimal price, String userType, BigDecimal expected) {
BigDecimal discount = calculator.calculate(price, userType);
assertThat(discount).isEqualByComparingTo(expected);
}
```
---
### 2.2 自动生成边界测试的Prompt
```markdown
请为这个类生成边界条件测试,使用参数化测试覆盖:
1. 数值边界MIN_VALUE, -1, 0, 1, MAX_VALUE
2. 字符串边界null, "", "a", 最大长度, 特殊字符🔑
3. 集合边界null, empty, 1 element, max size
4. 时间边界MIN, epoch, now, MAX
要求:
- 使用JUnit 5 @ParameterizedTest
- 每个边界值一个测试用例
- 验证异常处理
- 命名shouldHandleXxx_whenYxxIsBoundaryValue
类代码:
[粘贴代码]
```
---
## 🔧 第三类Mock使用不当重构
### 3.1 Mock决策树
```
需要Mock吗
├─ 是第三方外部服务?(支付/短信/邮件)
│ └─ 是 → 可以Mock
├─ 是基础设施?(数据库/缓存/消息队列)
│ └─ 是 → 用Testcontainers不要Mock
├─ 是核心业务逻辑Service/Controller
│ └─ 是 → 禁止Mock用真实实现
└─ 否 → 不Mock
```
**错误示例**过度Mock
```java
@SpringBootTest
class OrderServiceTest {
@MockBean
private OrderRepository orderRepository; // ❌ 不应该Mock Repository
@MockBean
private InventoryService inventoryService; // ⚠️ 可以Spy
@MockBean
private PaymentService paymentService; // ✅ 外部服务可以Mock
@Test
void shouldCreateOrder() {
when(orderRepository.save(any())).thenReturn(mockOrder); // 测试了Mock
when(inventoryService.checkStock(any())).thenReturn(true);
when(paymentService.charge(any(), any())).thenReturn(success);
Order result = orderService.create(request);
// 这个测试其实什么都没验证!
assertNotNull(result);
}
}
```
**正确做法**
```java
@SpringBootTest
@Testcontainers
class OrderServiceTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>();
@Autowired
private OrderRepository orderRepository; // ✅ 真实Repository
@SpyBean
private InventoryService inventoryService; // ✅ Spy部分方法
@MockBean
private PaymentService paymentService; // ✅ 外部服务Mock
@Test
void shouldCreateOrder_andSaveToDatabase() {
// Given
OrderRequest request = createOrderRequest();
when(paymentService.charge(any(), any())).thenReturn(success);
// When
Order result = orderService.create(request);
// Then
// 验证真实数据库写入
Order saved = orderRepository.findById(result.getId()).orElseThrow();
assertThat(saved.getStatus()).isEqualTo("PAID");
assertThat(saved.getTotal()).isEqualByComparingTo(request.getTotal());
// 验证真实调用外部服务
verify(paymentService).charge(
eq(request.getUserId()),
eq(request.getTotal())
);
}
}
```
**Mock审计脚本**
```bash
#!/bin/bash
# mock-audit.sh
echo "=== Mock审计报告 ==="
# 1. 统计MockBean数量
echo "1. MockBean统计"
grep -r "@MockBean" src/test/java --include="*.java" | wc -l
# 2. 检查Repository Mock
echo "2. Repository Mock应该为0"
grep -r "@MockBean.*Repository" src/test/java --include="*.java" | wc -l
# 3. 检查Service Mock
echo "3. Service Mock应该<5"
grep -r "@MockBean.*Service" src/test/java --include="*.java" | wc -l
# 4. 计算Mock比例
total_beans=$(grep -r "@MockBean\|@Autowired\|@SpyBean" src/test/java --include="*.java" | wc -l)
mock_beans=$(grep -r "@MockBean" src/test/java --include="*.java" | wc -l)
ratio=$((mock_beans * 100 / total_beans))
echo "4. Mock比例${ratio}%"
if [ $ratio -gt 50 ]; then
echo "⚠️ Warning: Mock比例过高建议重构"
fi
```
---
## 📊 第四类:性能测试缺失(补充)
### 4.1 AI常忽视的性能测试
**错误**(没有性能测试)
```java
@Test
void shouldProcessLargeFile() {
byte[] largeFile = new byte[10 * 1024 * 1024]; // 10MB
// 只测试功能,不测试性能
}
```
**正确做法**
```java
@Test
void shouldProcessLargeFile_withinTimeLimit() {
byte[] largeFile = createLargeFile(10 * 1024 * 1024); // 10MB
long start = System.currentTimeMillis();
ProcessResult result = fileProcessor.process(largeFile);
long duration = System.currentTimeMillis() - start;
// 性能断言
assertThat(duration).isLessThan(1000); // 必须在1秒内完成
assertThat(result).isNotNull();
assertThat(result.getProcessedBytes()).isEqualTo(largeFile.length);
}
@Test
void shouldHandleConcurrentRequests() throws InterruptedException {
int threadCount = 10;
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
service.process(createRequest());
successCount.incrementAndGet();
} finally {
latch.countDown();
}
}).start();
}
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertThat(successCount.get()).isEqualTo(threadCount);
}
```
---
## 📝 第五类:可读性/可维护性差(重构)
### 5.1 测试命名反模式
**AI常生成**
```java
@Test // ❌ 无法看出测试什么
void test1() { }
@Test // ❌ 不清晰
void shouldWork() { }
@Test // ❌ 没有Given/When
void createUserTest() { }
```
**正确命名**
```java
// 格式should[预期行为]_when[条件/场景]
@Test
void shouldReturnUserDetails_whenUserExists() { }
@Test
void shouldThrowNotFoundException_whenUserDoesNotExist() { }
@Test
void shouldSendWelcomeEmail_whenNewUserRegisters() { }
// 复杂场景
@Test
void shouldCalculateDiscountedPrice_whenUserIsVIP_andOrderExceeds1000() { }
```
### 5.2 Given-When-Then结构
**标准结构**
```java
@Test
void shouldDeactivateAccount_whenUserRequestsDeletion() {
// Given - 准备数据和状态
User user = createActiveUser("user123");
given(userRepository.findById("user123")).willReturn(Optional.of(user));
// When - 执行操作
accountService.deactivateAccount("user123");
// Then - 验证结果和副作用
assertThat(user.isActive()).isFalse();
verify(userRepository).save(user);
verify(emailService).sendDeactivationConfirmation("user123");
verify(cacheManager).evict("user:user123");
}
```
---
## 🔍 自动化检查清单
### 创建检查脚本
```bash
#!/bin/bash
# testing-quality-check.sh
echo "🧪 测试质量检查报告"
echo "===================="
# 1. 虚假测试检查
echo "1. 检查虚假测试..."
fake_tests=$(grep -r "assertTrue(true)\|assertFalse(false)\|assertNotNull(new" src/test/java --include="*.java" | wc -l)
echo " 找到 $fake_tests 个虚假断言"
# 2. 边界测试检查
echo "2. 检查边界条件..."
boundary_tests=$(grep -r "MIN_VALUE\|MAX_VALUE\|null.*empty" src/test/java --include="*.java" | wc -l)
echo " 边界测试: $boundary_tests"
# 3. Mock比例检查
echo "3. 检查Mock比例..."
total=$(find src/test/java -name "*Test.java" | wc -l)
mocks=$(grep -r "@MockBean" src/test/java --include="*.java" | wc -l)
if [ $total -gt 0 ]; then
ratio=$((mocks * 100 / total))
echo " Mock比例: ${ratio}%"
fi
# 4. 参数化测试检查
echo "4. 检查参数化测试..."
param_tests=$(grep -r "@ParameterizedTest\|@CsvSource" src/test/java --include="*.java" | wc -l)
echo " 参数化测试: $param_tests"
# 5. 命名规范检查
echo "5. 检查测试命名..."
incorrect=$(grep -r "void test[0-9]*\|void shouldWork\|void .*Test()" src/test/java --include="*.java" | wc -l)
echo " 命名不规范: $incorrect"
echo ""
echo "===================="
echo "检查完成!"
```
---
## ✅ 检查清单模板
### 每次生成测试后检查
**功能性**
- [ ] 测试验证真实业务逻辑,不只是框架代码
- [ ] 每个测试至少2个有意义的断言
- [ ] 测试包含边界条件null/空/极值)
- [ ] 测试包含异常场景
**质量**
- [ ] Mock比例 < 50%
- [ ] Service/Controller未Mock
- [ ] Repository使用Testcontainers
- [ ] 分支覆盖率 > 60%
**可维护性**
- [ ] 使用should_when命名格式
- [ ] 使用Given-When-Then结构
- [ ] 没有硬编码值
- [ ] 参数化测试替代重复测试
**性能/并发**
- [ ] 大对象处理有性能断言
- [ ] 并发场景有测试
- [ ] 没有资源泄漏
---
## 🎯 AI生成测试Prompt优化
### 原Prompt容易生成虚假测试
```
为UserService生成单元测试
```
### 优化后Prompt生成高质量测试
```
为UserService生成生产级单元测试要求
1. 使用真实数据库Testcontainers禁止Mock Repository
2. 每个测试至少2个断言验证返回值和副作用
3. 使用参数化测试覆盖边界条件null, 空值, 极值, 并发
4. 测试名使用should_when格式
5. 包含Given-When-Then结构
6. 验证异常场景和错误处理
7. 包含性能断言(执行时间<100ms
8. 不要测试getter/setter/构造函数
代码:
[粘贴代码]
生成测试时请思考:
- 这个测试在生产环境有用吗?
- 能发现真实bug吗
- 维护成本高吗?
```
---
## 📈 质量评分标准
| 维度 | 权重 | 优秀 | 良好 | 需改进 |
|------|------|------|------|--------|
| 功能性 | 30% | 无虚假测试,验证业务逻辑 | 少量框架测试 | 大量getter/setter测试 |
| 边界覆盖 | 25% | 系统化边界测试 | 常见边界测试 | 缺少边界测试 |
| Mock使用 | 20% | 比例<30%,核心真实 | 比例<50% | 比例>70% |
| 可维护性 | 15% | 参数化,无重复 | 有重复但清晰 | 大量重复代码 |
| 性能/并发 | 10% | 有性能+并发测试 | 有性能测试 | 无性能测试 |
**评分**
- A级(90-100):生产就绪
- B级(80-89):优秀
- C级(70-79):良好,可优化
- D级(60-69):及格,需改进
- F级(<60):不合格,需重写
---
## 🆘 快速修复指南
### 问题1太多getter/setter测试
```bash
# 自动识别并标记
find src/test/java -name "*Test.java" -exec grep -l "shouldReturn.*whenGet\|shouldSet.*whenSet" {} \; | xargs -I {} echo "Review: {}"
```
### 问题2Mock比例过高
```bash
# 识别过度Mock的测试
find src/test/java -name "*Test.java" -exec sh -c 'count=$(grep -c "@MockBean" "$1"); if [ "$count" -gt 5 ]; then echo "$1: $count mocks"; fi' _ {} \;
```
### 问题3缺少边界测试
```bash
# 识别缺少边界测试的类
find src/test/java -name "*Test.java" -exec sh -c 'if ! grep -q "ParameterizedTest\|MIN_VALUE\|MAX_VALUE\|null" "$1"; then echo "Missing boundary: $1"; fi' _ {} \;
```
---
**立即可用快速提升AI生成测试质量** 🚀