- 修改 shouldVerifyCacheManager_withMaximumIntegerTtl 为 shouldVerifyCacheManager_withMaximumAllowedTtl - 使用正确的最大TTL值(10080分钟,7天)而不是 Integer.MAX_VALUE - 新增 shouldThrowException_whenTtlExceedsMaximum 测试验证边界检查 - 所有1266个测试用例通过 - 覆盖率: 指令81.89%, 行88.48%, 分支51.55% docs: 添加项目状态报告 - 生成 PROJECT_STATUS_REPORT.md 详细记录项目当前状态 - 包含质量指标、已完成功能、待办事项和技术债务
1618 lines
50 KiB
Markdown
1618 lines
50 KiB
Markdown
# 测试最佳实践检查清单
|
||
|
||
> 基于蚊子项目1210+测试的真实经验总结
|
||
>
|
||
> 本文档旨在帮助识别和规避AI生成测试中常见的陷阱,提升测试质量和可维护性。
|
||
|
||
## 📋 目录
|
||
|
||
1. [AI测试陷阱识别清单](#1-ai测试陷阱识别清单)
|
||
2. [测试质量评估矩阵](#2-测试质量评估矩阵)
|
||
3. [可维护性检查清单](#3-可维护性检查清单)
|
||
4. [性能测试最佳实践](#4-性能测试最佳实践)
|
||
5. [并发测试策略](#5-并发测试策略)
|
||
6. [测试命名与文档规范](#6-测试命名与文档规范)
|
||
7. [Mock使用准则](#7-mock使用准则)
|
||
8. [断言最佳实践](#8-断言最佳实践)
|
||
9. [边界条件系统化方法](#9-边界条件系统化方法)
|
||
10. [持续改进检查表](#10-持续改进检查表)
|
||
|
||
---
|
||
|
||
## 1. AI测试陷阱识别清单
|
||
|
||
### 1.1 过度测试框架本身 🚫
|
||
|
||
#### 问题描述
|
||
AI常生成测试Lombok生成的getter/setter、构造函数或框架代码的测试,这些测试毫无意义。
|
||
|
||
#### ❌ 错误示例
|
||
```java
|
||
@Test
|
||
void testGetterSetter() {
|
||
User user = new User();
|
||
user.setName("test");
|
||
assertEquals("test", user.getName()); // 测试Lombok生成的方法
|
||
}
|
||
|
||
@Test
|
||
void testConstructor() {
|
||
User user = new User("id", "name");
|
||
assertNotNull(user); // 测试对象能被创建
|
||
assertEquals("id", user.getId()); // 测试简单赋值
|
||
}
|
||
```
|
||
|
||
#### ✅ 正确示例
|
||
```java
|
||
@Test
|
||
void shouldDetectDuplicateUsers_whenRegisteringWithExistingEmail() {
|
||
// 测试业务逻辑,而非框架功能
|
||
userRepository.save(new User("existing@test.com"));
|
||
|
||
DuplicateException exception = assertThrows(
|
||
DuplicateException.class,
|
||
() -> userService.register(new RegistrationRequest("existing@test.com"))
|
||
);
|
||
|
||
assertEquals("USER_EXISTS", exception.getErrorCode());
|
||
}
|
||
```
|
||
|
||
#### 检查方法
|
||
- [ ] 是否测试了框架生成的代码?
|
||
- [ ] 是否测试了无业务逻辑的简单赋值?
|
||
- [ ] 是否测试了编译器会保证的行为?
|
||
|
||
#### 自动化规则
|
||
```bash
|
||
# 查找可疑的DTO测试
|
||
grep -r "testGetter\|testSetter\|testConstructor" src/test --include="*.java"
|
||
|
||
# 查找Lombok类的无意义测试
|
||
find src/main -name "*.java" -exec grep -l "@Data\|@Getter\|@Setter" {} \; | \
|
||
xargs -I {} basename {} .java | \
|
||
xargs -I {} find src/test -name "{}Test.java" -exec grep -l "get.*()\|set.*()" {} \;
|
||
```
|
||
|
||
---
|
||
|
||
### 1.2 虚假断言 🚫
|
||
|
||
#### 问题描述
|
||
断言永远不会失败,或只验证显而易见的事情,无法真正验证业务逻辑。
|
||
|
||
#### ❌ 错误示例
|
||
```java
|
||
@Test
|
||
void testCreateObject() {
|
||
Object obj = new Object();
|
||
assertNotNull(obj); // 永远不会失败
|
||
}
|
||
|
||
@Test
|
||
void testTrueIsTrue() {
|
||
assertTrue(true); // 无意义断言
|
||
}
|
||
|
||
@Test
|
||
void testServiceCalled() {
|
||
service.doSomething();
|
||
verify(service).doSomething(); // 只验证方法被调用
|
||
}
|
||
```
|
||
|
||
#### ✅ 正确示例
|
||
```java
|
||
@Test
|
||
void shouldCalculateCorrectReward_whenUserCompletesHighValueActivity() {
|
||
// Given
|
||
Activity activity = Activity.builder()
|
||
.rewardPoints(100)
|
||
.multiplier(2.0)
|
||
.build();
|
||
|
||
// When
|
||
Reward reward = rewardCalculator.calculate(activity, user);
|
||
|
||
// Then
|
||
assertEquals(200, reward.getPoints()); // 验证实际业务值
|
||
assertEquals("PREMIUM", reward.getTier()); // 验证业务状态
|
||
assertNotNull(reward.getAwardedAt()); // 验证副作用
|
||
}
|
||
```
|
||
|
||
#### 检查方法
|
||
- [ ] 断言是否可能失败?
|
||
- [ ] 是否验证了具体的业务值?
|
||
- [ ] 是否验证了状态变化?
|
||
|
||
#### 自动化规则
|
||
```bash
|
||
# 查找虚假断言
|
||
grep -r "assertNotNull(new\|assertTrue(true\|assertFalse(false)" src/test --include="*.java"
|
||
|
||
# 查找只验证调用的测试
|
||
grep -r "verify(.*)\.doSomething\|verify(.*)\.save\|verify(.*)\.find" src/test --include="*.java" | \
|
||
grep -v "assert\|verify.*times"
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 测试质量评估矩阵
|
||
|
||
### 2.1 质量维度评分表
|
||
|
||
| 维度 | 权重 | 优秀(5) | 良好(4) | 合格(3) | 较差(2) | 不合格(1) |
|
||
|------|------|---------|---------|---------|---------|-----------|
|
||
| **业务价值** | 30% | 验证核心业务流程 | 验证重要业务规则 | 验证一般功能 | 验证边缘功能 | 无业务价值 |
|
||
| **断言质量** | 25% | 多维度精确验证 | 验证具体业务值 | 基本断言 | 模糊断言 | 虚假断言 |
|
||
| **边界覆盖** | 20% | 全面边界测试 | 主要边界覆盖 | 部分边界 | 极少边界 | 无边界测试 |
|
||
| **可读性** | 15% | 自文档化+注释 | 清晰命名结构 | 基本可读 | 难以理解 | 无法维护 |
|
||
| **独立性** | 10% | 完全独立 | 偶尔共享setup | 部分依赖 | 强依赖 | 相互依赖 |
|
||
|
||
### 2.2 测试分级标准
|
||
|
||
```
|
||
A级 (90-100分): 生产级测试,值得作为范例
|
||
B级 (75-89分): 良好测试,可以接受
|
||
C级 (60-74分): 及格测试,需要改进
|
||
D级 (40-59分): 问题测试,必须重构
|
||
F级 (<40分): 无效测试,应该删除
|
||
```
|
||
|
||
### 2.3 评估检查清单
|
||
|
||
- [ ] 测试是否明确验证了业务需求?
|
||
- [ ] 失败时能否快速定位问题?
|
||
- [ ] 是否覆盖了正常、异常、边界三种情况?
|
||
- [ ] 新开发者能否理解测试意图?
|
||
- [ ] 修改被测代码后,测试能否正确失败?
|
||
- [ ] 测试执行时间是否合理(<1秒)?
|
||
- [ ] 是否存在测试间的隐式依赖?
|
||
|
||
### 2.4 自动化评估脚本
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# test-quality-check.sh
|
||
|
||
echo "=== 测试质量快速评估 ==="
|
||
|
||
# 1. 检查虚假断言
|
||
echo "1. 虚假断言检查:"
|
||
grep -r "assertTrue(true)\|assertFalse(false)\|assertNotNull(new" src/test --include="*.java" | wc -l
|
||
echo " 发现疑似虚假断言数量"
|
||
|
||
# 2. 检查getter/setter测试
|
||
echo "2. Getter/Setter测试检查:"
|
||
grep -r "testGetter\|testSetter\|getId()\|setName" src/test --include="*.java" | wc -l
|
||
echo " 发现疑似框架测试数量"
|
||
|
||
# 3. 检查测试复杂度
|
||
echo "3. 测试复杂度检查:"
|
||
find src/test -name "*Test.java" -exec wc -l {} \; | sort -n | tail -5
|
||
echo " 最长的5个测试文件"
|
||
|
||
# 4. 检查断言密度
|
||
echo "4. 断言密度检查:"
|
||
for file in $(find src/test -name "*Test.java" | head -20); do
|
||
tests=$(grep -c "@Test" "$file" 2>/dev/null || echo 0)
|
||
asserts=$(grep -c "assert" "$file" 2>/dev/null || echo 0)
|
||
if [ "$tests" -gt 0 ]; then
|
||
density=$(echo "scale=2; $asserts / $tests" | bc)
|
||
echo " $file: $asserts assertions / $tests tests = $density"
|
||
fi
|
||
done
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 可维护性检查清单
|
||
|
||
### 3.1 硬编码值问题
|
||
|
||
#### 问题描述
|
||
测试中使用魔法数字和字符串,导致测试脆弱且难以理解。
|
||
|
||
#### ❌ 错误示例
|
||
```java
|
||
@Test
|
||
void testCalculation() {
|
||
assertEquals(1000, service.calculate(500, 2)); // 魔法数字
|
||
assertEquals("ERROR_001", exception.getCode()); // 无意义错误码
|
||
}
|
||
```
|
||
|
||
#### ✅ 正确示例
|
||
```java
|
||
class RewardCalculationTest {
|
||
private static final int BASE_POINTS = 500;
|
||
private static final int MULTIPLIER = 2;
|
||
private static final int EXPECTED_REWARD = 1000;
|
||
private static final String ERROR_INVALID_MULTIPLIER = "ERR_MULTIPLIER_NEGATIVE";
|
||
|
||
@Test
|
||
void shouldCalculateReward_whenValidMultiplierProvided() {
|
||
int result = service.calculate(BASE_POINTS, MULTIPLIER);
|
||
assertEquals(EXPECTED_REWARD, result);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 检查方法
|
||
- [ ] 所有字面量是否有明确含义?
|
||
- [ ] 是否使用了有意义的常量名?
|
||
- [ ] 魔法数字是否分散在测试各处?
|
||
|
||
---
|
||
|
||
### 3.2 重复代码问题
|
||
|
||
#### 问题描述
|
||
多个测试中重复相同的setup和断言逻辑,难以维护。
|
||
|
||
#### ❌ 错误示例
|
||
```java
|
||
@Test
|
||
void testCreateUser() {
|
||
User user = new User();
|
||
user.setName("test");
|
||
user.setEmail("test@test.com");
|
||
user.setCreatedAt(LocalDateTime.now());
|
||
// ... 重复10次
|
||
}
|
||
|
||
@Test
|
||
void testUpdateUser() {
|
||
User user = new User();
|
||
user.setName("test");
|
||
user.setEmail("test@test.com");
|
||
user.setCreatedAt(LocalDateTime.now());
|
||
// ... 同样的重复
|
||
}
|
||
```
|
||
|
||
#### ✅ 正确示例
|
||
```java
|
||
class UserServiceTest {
|
||
private User testUser;
|
||
|
||
@BeforeEach
|
||
void setUp() {
|
||
testUser = createDefaultUser();
|
||
}
|
||
|
||
private User createDefaultUser() {
|
||
return User.builder()
|
||
.name("Test User")
|
||
.email("test@test.com")
|
||
.createdAt(LocalDateTime.now())
|
||
.build();
|
||
}
|
||
|
||
private void assertUserHasDefaultValues(User user) {
|
||
assertAll("User default values",
|
||
() -> assertEquals("Test User", user.getName()),
|
||
() -> assertEquals("test@test.com", user.getEmail()),
|
||
() -> assertNotNull(user.getCreatedAt())
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 检查方法
|
||
- [ ] 是否使用了@BeforeEach/@BeforeAll?
|
||
- [ ] 是否有测试数据工厂类?
|
||
- [ ] 是否有共享的断言方法?
|
||
|
||
---
|
||
|
||
### 3.3 测试间依赖问题
|
||
|
||
#### 问题描述
|
||
测试之间存在隐式依赖,执行顺序影响结果,难以并行运行。
|
||
|
||
#### ❌ 错误示例
|
||
```java
|
||
@Test
|
||
@Order(1) // 强制顺序本身就是问题信号
|
||
void testCreate() {
|
||
userService.createUser("test");
|
||
}
|
||
|
||
@Test
|
||
@Order(2) // 依赖testCreate
|
||
void testRead() {
|
||
User user = userService.findByName("test"); // 依赖前面的测试数据
|
||
assertNotNull(user);
|
||
}
|
||
```
|
||
|
||
#### ✅ 正确示例
|
||
```java
|
||
@Test
|
||
void shouldCreateAndRetrieveUser_independently() {
|
||
// Given - 每个测试自己准备数据
|
||
String uniqueName = "test_" + UUID.randomUUID();
|
||
|
||
// When
|
||
userService.createUser(uniqueName);
|
||
User created = userService.findByName(uniqueName);
|
||
|
||
// Then
|
||
assertNotNull(created);
|
||
|
||
// Cleanup - 自己清理
|
||
userService.delete(created.getId());
|
||
}
|
||
```
|
||
|
||
#### 检查方法
|
||
- [ ] 是否使用了@Order注解?(危险信号)
|
||
- [ ] 测试能否独立运行?
|
||
- [ ] 是否共享可变状态?
|
||
- [ ] 能否并行执行(`mvn test -Dparallel`)?
|
||
|
||
---
|
||
|
||
## 4. 性能测试最佳实践
|
||
|
||
### 4.1 性能测试类型
|
||
|
||
```java
|
||
/**
|
||
* 1. 基线测试 - 记录正常性能指标
|
||
*/
|
||
@Test
|
||
void shouldCompleteWithinBaseline_whenProcessingStandardLoad() {
|
||
// Given
|
||
List<Data> standardData = DataFactory.create(100); // 标准数据量
|
||
|
||
// When
|
||
long start = System.currentTimeMillis();
|
||
Result result = processor.process(standardData);
|
||
long duration = System.currentTimeMillis() - start;
|
||
|
||
// Then
|
||
assertTrue(duration < 500, // 基线: <500ms
|
||
"Processing took " + duration + "ms, expected <500ms");
|
||
assertNotNull(result);
|
||
}
|
||
|
||
/**
|
||
* 2. 负载测试 - 测试预期峰值
|
||
*/
|
||
@Test
|
||
void shouldHandlePeakLoad_whenConcurrentRequestsArrive() {
|
||
int concurrentUsers = 100;
|
||
int requestsPerUser = 10;
|
||
|
||
ExecutorService executor = Executors.newFixedThreadPool(concurrentUsers);
|
||
CountDownLatch latch = new CountDownLatch(concurrentUsers * requestsPerUser);
|
||
AtomicInteger successCount = new AtomicInteger(0);
|
||
|
||
// When
|
||
for (int i = 0; i < concurrentUsers; i++) {
|
||
executor.submit(() -> {
|
||
for (int j = 0; j < requestsPerUser; j++) {
|
||
try {
|
||
Response response = api.call();
|
||
if (response.isSuccess()) {
|
||
successCount.incrementAndGet();
|
||
}
|
||
} finally {
|
||
latch.countDown();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Then
|
||
assertTrue(latch.await(30, TimeUnit.SECONDS));
|
||
assertEquals(concurrentUsers * requestsPerUser, successCount.get());
|
||
|
||
executor.shutdown();
|
||
}
|
||
|
||
/**
|
||
* 3. 压力测试 - 测试系统极限
|
||
*/
|
||
@Test
|
||
void shouldDegradeGracefully_whenExceedingCapacity() {
|
||
// 逐步增加负载直到系统出现性能下降
|
||
for (int load : Arrays.asList(100, 500, 1000, 2000, 5000)) {
|
||
PerformanceResult result = runWithLoad(load);
|
||
|
||
if (result.getErrorRate() > 0.05) { // 5%错误率阈值
|
||
// 记录最大容量
|
||
log.info("System capacity limit: {} requests", load);
|
||
assertTrue(result.isGracefulDegradation(),
|
||
"System should degrade gracefully");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 4. 稳定性测试 - 长时间运行
|
||
*/
|
||
@Test
|
||
void shouldMaintainPerformance_overExtendedPeriod() {
|
||
long testDuration = TimeUnit.MINUTES.toMillis(5);
|
||
long startTime = System.currentTimeMillis();
|
||
List<Long> responseTimes = new ArrayList<>();
|
||
|
||
while (System.currentTimeMillis() - startTime < testDuration) {
|
||
long callStart = System.currentTimeMillis();
|
||
api.call();
|
||
responseTimes.add(System.currentTimeMillis() - callStart);
|
||
|
||
Thread.sleep(100); // 模拟真实间隔
|
||
}
|
||
|
||
// 分析响应时间趋势
|
||
double avgResponseTime = responseTimes.stream()
|
||
.mapToLong(Long::longValue)
|
||
.average()
|
||
.orElse(0);
|
||
|
||
double p95ResponseTime = calculatePercentile(responseTimes, 95);
|
||
|
||
assertTrue(avgResponseTime < 200, "Average response time too high");
|
||
assertTrue(p95ResponseTime < 500, "P95 response time too high");
|
||
}
|
||
```
|
||
|
||
### 4.2 性能测试检查清单
|
||
|
||
- [ ] 是否建立了性能基线?
|
||
- [ ] 是否测试了预期峰值负载?
|
||
- [ ] 是否测试了系统极限(压力测试)?
|
||
- [ ] 是否进行了长时间稳定性测试?
|
||
- [ ] 是否测量了响应时间分布(P50, P95, P99)?
|
||
- [ ] 是否监控了资源使用(CPU, 内存, IO)?
|
||
- [ ] 是否有性能回归检测机制?
|
||
|
||
### 4.3 性能反模式
|
||
|
||
```java
|
||
// ❌ 在单元测试中测试性能
|
||
@Test
|
||
void testPerformance() { // 错误:单元测试不该测性能
|
||
for (int i = 0; i < 1000000; i++) {
|
||
service.doSomething();
|
||
}
|
||
}
|
||
|
||
// ❌ 不稳定的性能断言
|
||
@Test
|
||
void testResponseTime() {
|
||
long start = System.currentTimeMillis();
|
||
service.call();
|
||
long duration = System.currentTimeMillis() - start;
|
||
assertTrue(duration < 10); // 太严格,环境差异会导致失败
|
||
}
|
||
|
||
// ❌ 只测试吞吐量不测试延迟
|
||
@Test
|
||
void testThroughput() {
|
||
int count = 0;
|
||
long end = System.currentTimeMillis() + 1000;
|
||
while (System.currentTimeMillis() < end) {
|
||
service.call();
|
||
count++;
|
||
}
|
||
assertTrue(count > 1000); // 只关心数量不关心单个请求质量
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 并发测试策略
|
||
|
||
### 5.1 并发测试基本原则
|
||
|
||
```java
|
||
/**
|
||
* 并发测试黄金法则:
|
||
* 1. 不要依赖Thread.sleep() - 使用CountDownLatch/CyclicBarrier
|
||
* 2. 不要假设执行顺序 - 使用同步原语控制
|
||
* 3. 测试竞争条件,而不仅仅是并发执行
|
||
* 4. 验证最终一致性和不变量
|
||
*/
|
||
```
|
||
|
||
### 5.2 常见并发测试模式
|
||
|
||
```java
|
||
/**
|
||
* 模式1: 测试竞态条件 - 库存扣减
|
||
*/
|
||
@Test
|
||
void shouldPreventOverselling_whenConcurrentPurchases() throws InterruptedException {
|
||
// Given
|
||
Product product = productRepository.save(
|
||
Product.builder().stock(10).build()
|
||
);
|
||
int concurrentBuyers = 15;
|
||
ExecutorService executor = Executors.newFixedThreadPool(concurrentBuyers);
|
||
CountDownLatch startLatch = new CountDownLatch(1);
|
||
CountDownLatch completeLatch = new CountDownLatch(concurrentBuyers);
|
||
AtomicInteger successCount = new AtomicInteger(0);
|
||
AtomicInteger failCount = new AtomicInteger(0);
|
||
|
||
// When - 所有线程同时开始
|
||
for (int i = 0; i < concurrentBuyers; i++) {
|
||
executor.submit(() -> {
|
||
try {
|
||
startLatch.await(); // 等待统一信号
|
||
try {
|
||
purchaseService.buy(product.getId(), 1);
|
||
successCount.incrementAndGet();
|
||
} catch (OutOfStockException e) {
|
||
failCount.incrementAndGet();
|
||
}
|
||
} catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
} finally {
|
||
completeLatch.countDown();
|
||
}
|
||
});
|
||
}
|
||
|
||
startLatch.countDown(); // 同时触发所有线程
|
||
completeLatch.await(10, TimeUnit.SECONDS);
|
||
|
||
// Then - 验证不变量
|
||
Product updated = productRepository.findById(product.getId()).orElseThrow();
|
||
assertEquals(10, successCount.get(), "只有10个应该成功");
|
||
assertEquals(5, failCount.get(), "5个应该失败");
|
||
assertEquals(0, updated.getStock(), "库存应该为0");
|
||
}
|
||
|
||
/**
|
||
* 模式2: 测试死锁
|
||
*/
|
||
@Test
|
||
void shouldNotDeadlock_whenNestedLockAcquisition() {
|
||
Account account1 = new Account("A1", 1000);
|
||
Account account2 = new Account("A2", 1000);
|
||
|
||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||
|
||
// 两个线程以不同顺序获取锁(经典的死锁场景)
|
||
Future<?> future1 = executor.submit(() ->
|
||
transferService.transfer(account1, account2, 100));
|
||
Future<?> future2 = executor.submit(() ->
|
||
transferService.transfer(account2, account1, 100));
|
||
|
||
// 验证不会死锁
|
||
assertDoesNotThrow(() -> {
|
||
future1.get(5, TimeUnit.SECONDS);
|
||
future2.get(5, TimeUnit.SECONDS);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 模式3: 测试可见性
|
||
*/
|
||
@Test
|
||
void shouldEnsureVisibility_whenMultipleThreadsReadWrite()
|
||
throws InterruptedException {
|
||
ConcurrentProcessor processor = new ConcurrentProcessor();
|
||
int writerCount = 5;
|
||
int readerCount = 10;
|
||
int iterations = 1000;
|
||
|
||
ExecutorService executor = Executors.newFixedThreadPool(writerCount + readerCount);
|
||
CountDownLatch latch = new CountDownLatch(writerCount + readerCount);
|
||
AtomicInteger visibilityViolations = new AtomicInteger(0);
|
||
|
||
// Writers
|
||
for (int i = 0; i < writerCount; i++) {
|
||
final int writerId = i;
|
||
executor.submit(() -> {
|
||
for (int j = 0; j < iterations; j++) {
|
||
processor.write(writerId, j);
|
||
}
|
||
latch.countDown();
|
||
});
|
||
}
|
||
|
||
// Readers
|
||
for (int i = 0; i < readerCount; i++) {
|
||
executor.submit(() -> {
|
||
for (int j = 0; j < iterations; j++) {
|
||
Data data = processor.read();
|
||
// 验证读取的数据一致性
|
||
if (!data.isConsistent()) {
|
||
visibilityViolations.incrementAndGet();
|
||
}
|
||
}
|
||
latch.countDown();
|
||
});
|
||
}
|
||
|
||
latch.await(30, TimeUnit.SECONDS);
|
||
assertEquals(0, visibilityViolations.get(),
|
||
"不应该出现可见性违规");
|
||
}
|
||
|
||
/**
|
||
* 模式4: 使用JCStress进行系统并发测试
|
||
*/
|
||
@JCStressTest
|
||
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "两者都看到完整写入")
|
||
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "一个看到写入")
|
||
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE, desc = "另一个看到写入")
|
||
@Outcome(id = "0, 0", expect = Expect.FORBIDDEN, desc = "都不看到写入是bug")
|
||
@State
|
||
public class VolatileVisibilityTest {
|
||
volatile int x;
|
||
volatile int y;
|
||
|
||
@Actor
|
||
public void actor1() {
|
||
x = 1;
|
||
y = 1;
|
||
}
|
||
|
||
@Actor
|
||
public void actor2(II_Result r) {
|
||
r.r1 = x;
|
||
r.r2 = y;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.3 并发测试检查清单
|
||
|
||
- [ ] 是否测试了竞态条件?
|
||
- [ ] 是否验证了不变量(最终一致性)?
|
||
- [ ] 是否使用了同步原语而非Thread.sleep()?
|
||
- [ ] 是否考虑了不同的线程交错?
|
||
- [ ] 是否测试了超时场景?
|
||
- [ ] 是否验证了资源清理?
|
||
- [ ] 是否使用JCStress等工具进行压力测试?
|
||
|
||
---
|
||
|
||
## 6. 测试命名与文档规范
|
||
|
||
### 6.1 测试命名最佳实践
|
||
|
||
#### 命名格式对比
|
||
|
||
| 格式 | 示例 | 推荐度 |
|
||
|------|------|--------|
|
||
| **should_when** | `shouldThrowException_whenInvalidInput` | ⭐⭐⭐⭐⭐ |
|
||
| **Given_When_Then** | `GivenValidUser_WhenRegister_ThenSuccess` | ⭐⭐⭐⭐ |
|
||
| **method_condition_result** | `registerUser_withInvalidEmail_throwsException` | ⭐⭐⭐⭐ |
|
||
| **testMethod** | `testRegister` | ⭐⭐ 不推荐 |
|
||
|
||
#### ✅ 优秀命名示例
|
||
|
||
```java
|
||
// 业务场景清晰
|
||
@Test
|
||
void shouldSendNotification_whenUserCompletesMilestone()
|
||
|
||
// 边界条件明确
|
||
@Test
|
||
void shouldRejectRequest_whenEmailExceeds255Characters()
|
||
|
||
// 异常场景具体
|
||
@Test
|
||
void shouldThrowDuplicateKeyException_whenRegisteringExistingEmail()
|
||
|
||
// 状态转换明确
|
||
@Test
|
||
void shouldTransitionFromPendingToActive_whenPaymentConfirmed()
|
||
|
||
// 并发场景清晰
|
||
@Test
|
||
void shouldMaintainConsistency_whenConcurrentUpdatesToSameEntity()
|
||
```
|
||
|
||
#### ❌ 糟糕命名示例
|
||
|
||
```java
|
||
@Test
|
||
void test1() // 无意义
|
||
|
||
@Test
|
||
void testUser() // 过于笼统
|
||
|
||
@Test
|
||
void testRegisterUserSuccess() // 没有when部分
|
||
|
||
@Test
|
||
void test() // 完全无信息
|
||
```
|
||
|
||
### 6.2 Given-When-Then结构
|
||
|
||
```java
|
||
@Test
|
||
void shouldCalculateDiscountedPrice_whenPremiumMemberOnSale() {
|
||
// ==================== GIVEN ====================
|
||
// 准备测试数据
|
||
User premiumUser = User.builder()
|
||
.id("user-123")
|
||
.membershipTier(MembershipTier.PREMIUM)
|
||
.registrationDate(LocalDate.of(2020, 1, 1))
|
||
.build();
|
||
|
||
Product saleProduct = Product.builder()
|
||
.id("prod-456")
|
||
.basePrice(new BigDecimal("100.00"))
|
||
.saleDiscount(new BigDecimal("0.20")) // 8折
|
||
.build();
|
||
|
||
// 配置mock
|
||
when(userService.findById("user-123")).thenReturn(premiumUser);
|
||
when(productService.findById("prod-456")).thenReturn(saleProduct);
|
||
|
||
// ==================== WHEN ====================
|
||
// 执行被测操作
|
||
PriceCalculationResult result = priceCalculator.calculate(
|
||
"user-123",
|
||
"prod-456",
|
||
2 // 购买2件
|
||
);
|
||
|
||
// ==================== THEN ====================
|
||
// 验证结果
|
||
assertAll("价格计算验证",
|
||
// 验证业务值
|
||
() -> assertEquals(new BigDecimal("144.00"), result.getFinalPrice(),
|
||
"Premium用户应该享受折上折: 100*0.8*0.9*2"),
|
||
|
||
// 验证折扣明细
|
||
() -> assertEquals(2, result.getAppliedDiscounts().size()),
|
||
() -> assertTrue(result.getAppliedDiscounts().contains("SALE_20%")),
|
||
() -> assertTrue(result.getAppliedDiscounts().contains("PREMIUM_10%")),
|
||
|
||
// 验证行为
|
||
() -> verify(userService).findById("user-123"),
|
||
() -> verify(productService).findById("prod-456")
|
||
);
|
||
}
|
||
```
|
||
|
||
### 6.3 测试文档注释规范
|
||
|
||
```java
|
||
/**
|
||
* 测试场景: 高并发下的库存扣减
|
||
*
|
||
* 业务规则:
|
||
* - 每个商品有固定库存
|
||
* - 多用户同时购买时,不应该超卖
|
||
* - 库存为0时,后续购买请求应该失败
|
||
*
|
||
* 测试策略:
|
||
* - 使用15个并发线程模拟15个用户同时购买
|
||
* - 商品初始库存为10
|
||
* - 验证最终只有10个购买成功
|
||
*
|
||
* 相关需求: REQ-001, REQ-045
|
||
* 关联缺陷: BUG-2024-1234
|
||
*
|
||
* @see InventoryService#deductStock(String, int)
|
||
* @since 1.2.0
|
||
*/
|
||
@Test
|
||
void shouldPreventOverselling_whenConcurrentPurchases() {
|
||
// ... 测试实现
|
||
}
|
||
```
|
||
|
||
### 6.4 检查清单
|
||
|
||
- [ ] 测试名是否描述了行为和条件?
|
||
- [ ] 是否使用了should_when格式?
|
||
- [ ] 是否包含Given-When-Then结构注释?
|
||
- [ ] 复杂测试是否有场景说明注释?
|
||
- [ ] 是否引用了相关需求和缺陷?
|
||
- [ ] 是否使用了assertAll组合相关断言?
|
||
|
||
---
|
||
|
||
## 7. Mock使用准则
|
||
|
||
### 7.1 Mock决策树
|
||
|
||
```
|
||
是否使用Mock?
|
||
├── 是: 外部依赖(数据库、API、消息队列)
|
||
├── 是: 不可控组件(随机数、时间、网络)
|
||
├── 是: 尚未实现的依赖
|
||
├── 是: 测试需要特定异常场景
|
||
├── 否: 业务逻辑本身(不应该Mock)
|
||
├── 否: 简单值对象
|
||
└── 否: 可以实际初始化的轻量对象
|
||
```
|
||
|
||
### 7.2 Mock最佳实践
|
||
|
||
```java
|
||
@ExtendWith(MockitoExtension.class)
|
||
class OrderServiceTest {
|
||
|
||
@Mock
|
||
private PaymentGateway paymentGateway; // ✅ 外部服务
|
||
|
||
@Mock
|
||
private InventoryService inventoryService; // ✅ 复杂依赖
|
||
|
||
@InjectMocks
|
||
private OrderService orderService; // 被测对象,不Mock
|
||
|
||
// ❌ 不应该Mock
|
||
// @Mock
|
||
// private PriceCalculator priceCalculator; // 核心业务逻辑
|
||
|
||
@Test
|
||
void shouldCompleteOrder_whenPaymentSucceeds() {
|
||
// Given - 配置外部依赖
|
||
when(paymentGateway.charge(any(PaymentRequest.class)))
|
||
.thenReturn(PaymentResult.success("txn-123"));
|
||
when(inventoryService.reserve(anyString(), anyInt()))
|
||
.thenReturn(InventoryResult.success());
|
||
|
||
// When - 执行被测业务
|
||
OrderResult result = orderService.placeOrder(createValidOrder());
|
||
|
||
// Then - 验证业务结果和交互
|
||
assertEquals(OrderStatus.COMPLETED, result.getStatus());
|
||
verify(paymentGateway).charge(argThat(req ->
|
||
req.getAmount().compareTo(new BigDecimal("100.00")) == 0
|
||
));
|
||
verify(inventoryService).reserve("SKU-001", 2);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.3 Mock反模式
|
||
|
||
```java
|
||
// ❌ 反模式1: Mock被测对象本身
|
||
@Test
|
||
void testOrderService() {
|
||
OrderService mockService = mock(OrderService.class);
|
||
when(mockService.placeOrder(any())).thenReturn(mockResult);
|
||
|
||
// 测试的是Mock,不是真实逻辑
|
||
assertNotNull(mockService.placeOrder(new Order()));
|
||
}
|
||
|
||
// ❌ 反模式2: 过度使用ArgumentCaptor
|
||
@Test
|
||
void testWithExcessiveCaptors() {
|
||
ArgumentCaptor<PaymentRequest> captor1 = ArgumentCaptor.forClass(PaymentRequest.class);
|
||
ArgumentCaptor<String> captor2 = ArgumentCaptor.forClass(String.class);
|
||
ArgumentCaptor<Integer> captor3 = ArgumentCaptor.forClass(Integer.class);
|
||
|
||
// ... 测试代码 ...
|
||
|
||
// 过度验证实现细节,而非业务结果
|
||
verify(service).method(captor1.capture(), captor2.capture(), captor3.capture());
|
||
assertEquals("value1", captor1.getValue().getField1());
|
||
assertEquals("value2", captor2.getValue());
|
||
assertEquals(42, captor3.getValue());
|
||
}
|
||
|
||
// ❌ 反模式3: Mock Repository层(应该是集成测试)
|
||
@Test
|
||
void testWithMockedRepository() {
|
||
when(userRepository.findById("123")).thenReturn(Optional.of(mockUser));
|
||
|
||
// 测试的是Mock返回的数据,不是真实数据库交互
|
||
User user = userService.findById("123");
|
||
assertEquals(mockUser.getName(), user.getName());
|
||
}
|
||
|
||
// ❌ 反模式4: 不验证Mock交互
|
||
@Test
|
||
void testWithoutVerification() {
|
||
when(paymentGateway.charge(any())).thenReturn(success());
|
||
|
||
orderService.placeOrder(order);
|
||
|
||
// 缺少verify,不知道paymentGateway是否真的被调用
|
||
}
|
||
|
||
// ❌ 反模式5: 使用Mockito的一般匹配器测试具体值
|
||
@Test
|
||
void testWithGeneralMatchers() {
|
||
// 错误: 使用any()代替具体值
|
||
when(service.calculate(any(), any())).thenReturn(100);
|
||
|
||
// 无法验证是否正确传递了参数
|
||
assertEquals(100, service.calculate(5, 10)); // 实际应该验证5*10=50
|
||
assertEquals(100, service.calculate(999, 999)); // 错误值也返回100
|
||
}
|
||
```
|
||
|
||
### 7.4 Mock检查清单
|
||
|
||
- [ ] 只Mock外部依赖,不Mock核心业务逻辑
|
||
- [ ] 配置Mock时使用具体的参数匹配
|
||
- [ ] 验证Mock的交互(verify)
|
||
- [ ] 使用ArgumentCaptor时保持简洁
|
||
- [ ] Repository层使用真实数据库(集成测试)
|
||
- [ ] 考虑使用@Spy部分Mock
|
||
- [ ] 使用BDDMockito(given/willReturn/then/should)提升可读性
|
||
|
||
---
|
||
|
||
## 8. 断言最佳实践
|
||
|
||
### 8.1 断言金字塔
|
||
|
||
```
|
||
/\
|
||
/ \
|
||
/ 单 \
|
||
/ 个 \
|
||
/ 断言 \ ← 简单场景
|
||
/--------\
|
||
/ 多个 \
|
||
/ 断言 \ ← 常见场景
|
||
/--------------\
|
||
/ 组合断言 \
|
||
/ (assertAll) \ ← 复杂场景
|
||
/--------------------\
|
||
/ 自定义断言 \ ← 领域特定
|
||
/------------------------\
|
||
```
|
||
|
||
### 8.2 断言选择指南
|
||
|
||
| 场景 | 推荐断言 | 示例 |
|
||
|------|---------|------|
|
||
| 相等性 | assertEquals | `assertEquals(expected, actual)` |
|
||
| 同一性 | assertSame | `assertSame(singleton, result)` |
|
||
| 非空 | assertNotNull | `assertNotNull(user.getId())` |
|
||
| 布尔 | assertTrue/False | `assertTrue(user.isActive())` |
|
||
| 异常 | assertThrows | `assertThrows(InvalidException.class, () -> ...)` |
|
||
| 集合 | assertIterableEquals | `assertIterableEquals(expected, actual)` |
|
||
| 多条件 | assertAll | `assertAll("user", () -> ..., () -> ...)` |
|
||
| 超时 | assertTimeout | `assertTimeout(Duration.ofSeconds(1), () -> ...)` |
|
||
| 浮点数 | assertEquals(delta) | `assertEquals(3.14, result, 0.01)` |
|
||
|
||
### 8.3 高质量断言示例
|
||
|
||
```java
|
||
@Test
|
||
void shouldCreateUser_withAllFieldsProperlySet() {
|
||
User user = userService.createUser(new CreateUserRequest(
|
||
"john.doe@example.com",
|
||
"John Doe",
|
||
LocalDate.of(1990, 5, 15)
|
||
));
|
||
|
||
// ✅ 组合断言 - 一次性验证所有相关属性
|
||
assertAll("用户创建验证",
|
||
// 验证ID生成
|
||
() -> assertNotNull(user.getId(), "用户ID应该被生成"),
|
||
() -> assertTrue(user.getId().startsWith("USR"), "ID应该以USR开头"),
|
||
|
||
// 验证基本字段
|
||
() -> assertEquals("john.doe@example.com", user.getEmail()),
|
||
() -> assertEquals("John Doe", user.getName()),
|
||
|
||
// 验证派生字段
|
||
() -> assertEquals(34, user.getAge(), "年龄应该根据生日计算"),
|
||
|
||
// 验证默认值
|
||
() -> assertEquals(UserStatus.PENDING, user.getStatus()),
|
||
() -> assertNotNull(user.getCreatedAt()),
|
||
() -> assertTrue(user.getCreatedAt().isBefore(LocalDateTime.now().plusSeconds(1))),
|
||
|
||
// 验证关联数据
|
||
() -> assertNotNull(user.getProfile()),
|
||
() -> assertTrue(user.getRoles().contains(Role.USER))
|
||
);
|
||
}
|
||
|
||
@Test
|
||
void shouldThrowValidationException_whenEmailIsMalformed() {
|
||
InvalidRequestException exception = assertThrows(
|
||
InvalidRequestException.class,
|
||
() -> userService.createUser(new CreateUserRequest(
|
||
"invalid-email",
|
||
"John Doe",
|
||
null
|
||
)),
|
||
"应该抛出验证异常"
|
||
);
|
||
|
||
// ✅ 验证异常详情
|
||
assertAll("异常验证",
|
||
() -> assertEquals("VALIDATION_ERROR", exception.getErrorCode()),
|
||
() -> assertTrue(exception.getMessage().contains("email")),
|
||
() -> assertEquals(1, exception.getFieldErrors().size()),
|
||
() -> assertEquals("email", exception.getFieldErrors().get(0).getField())
|
||
);
|
||
}
|
||
|
||
@Test
|
||
void shouldReturnPagedResults_withCorrectPaginationMetadata() {
|
||
Page<User> page = userService.findAll(PageRequest.of(2, 10)); // 第3页,每页10条
|
||
|
||
// ✅ 验证复杂对象的所有方面
|
||
assertAll("分页结果验证",
|
||
// 内容验证
|
||
() -> assertEquals(10, page.getContent().size(), "应该返回10条记录"),
|
||
() -> assertTrue(page.getContent().stream().allMatch(u -> u.getId() != null)),
|
||
|
||
// 分页元数据验证
|
||
() -> assertEquals(2, page.getNumber(), "当前页码"),
|
||
() -> assertEquals(10, page.getSize(), "每页大小"),
|
||
() -> assertEquals(10, page.getNumberOfElements(), "当前页元素数"),
|
||
|
||
// 导航验证
|
||
() -> assertTrue(page.hasPrevious(), "应该有上一页"),
|
||
() -> assertTrue(page.hasNext(), "应该有下一页"),
|
||
() -> assertEquals(5, page.getTotalPages(), "总页数"),
|
||
() -> assertEquals(47, page.getTotalElements(), "总元素数"),
|
||
|
||
// 排序验证
|
||
() -> assertTrue(page.getSort().isSorted()),
|
||
() -> assertEquals("createdAt", page.getSort().iterator().next().getProperty())
|
||
);
|
||
}
|
||
```
|
||
|
||
### 8.4 断言反模式
|
||
|
||
```java
|
||
// ❌ 反模式1: 模糊消息
|
||
assertTrue(user.isValid(), "验证失败"); // 消息无用
|
||
assertEquals(expected, actual); // 无消息,失败时难调试
|
||
|
||
// ✅ 正确
|
||
assertTrue(user.isValid(), "用户应该有效,但实际状态: " + user.getStatus());
|
||
assertEquals(expected, actual, "订单总额应该匹配,订单ID: " + orderId);
|
||
|
||
// ❌ 反模式2: 多个独立assert
|
||
@Test
|
||
void testUser() {
|
||
assertNotNull(user);
|
||
assertEquals("John", user.getName());
|
||
assertTrue(user.isActive());
|
||
// 第一个失败,后面的不会执行,不知道还有多少问题
|
||
}
|
||
|
||
// ✅ 正确 - 使用assertAll
|
||
@Test
|
||
void testUser() {
|
||
assertAll("用户验证",
|
||
() -> assertNotNull(user),
|
||
() -> assertEquals("John", user.getName()),
|
||
() -> assertTrue(user.isActive())
|
||
);
|
||
}
|
||
|
||
// ❌ 反模式3: 在测试中try-catch
|
||
@Test
|
||
void testWithTryCatch() {
|
||
try {
|
||
service.doSomething();
|
||
fail("应该抛出异常"); // 容易忘记
|
||
} catch (Exception e) {
|
||
assertEquals("error", e.getMessage());
|
||
}
|
||
}
|
||
|
||
// ✅ 正确 - 使用assertThrows
|
||
@Test
|
||
void testWithAssertThrows() {
|
||
Exception e = assertThrows(Exception.class, () -> service.doSomething());
|
||
assertEquals("error", e.getMessage());
|
||
}
|
||
|
||
// ❌ 反模式4: 浮点数精确比较
|
||
assertEquals(0.1 + 0.2, result); // 可能失败: 0.30000000000000004
|
||
|
||
// ✅ 正确 - 使用delta
|
||
assertEquals(0.3, result, 0.001);
|
||
```
|
||
|
||
### 8.5 断言检查清单
|
||
|
||
- [ ] 断言消息是否清晰描述了期望?
|
||
- [ ] 是否使用assertAll组合相关断言?
|
||
- [ ] 异常测试是否使用了assertThrows?
|
||
- [ ] 浮点数比较是否使用了delta?
|
||
- [ ] 集合比较是否使用了适当的断言?
|
||
- [ ] 是否验证了所有重要的输出字段?
|
||
- [ ] 断言顺序是否合理(先验证前提条件)?
|
||
|
||
---
|
||
|
||
## 9. 边界条件系统化方法
|
||
|
||
### 9.1 边界值分析矩阵
|
||
|
||
```
|
||
输入类型 边界值 测试用例
|
||
─────────────────────────────────────────────────────────
|
||
数值范围 最小值-1, 最小值, -1, 0, 1, 99, 100, 101
|
||
最小值+1, 最大值-1, (范围0-100)
|
||
最大值, 最大值+1
|
||
|
||
字符串长度 空串, 1字符, "", "a", "ab",
|
||
最大长度-1, repeat("a", 254),
|
||
最大长度, 最大长度+1 repeat("a", 255),
|
||
repeat("a", 256)
|
||
|
||
集合/数组 空, 1元素, [], [1], [1,2],
|
||
最大容量-1, size=999, size=1000,
|
||
最大容量, 最大容量+1 size=1001
|
||
|
||
日期时间 最小日期, 最大日期, 1970-01-01,
|
||
闰年2月29日, 2038-01-19 (Y2K38)
|
||
时区边界 2024-02-29
|
||
|
||
枚举值 第一个, 最后一个, ENUM_FIRST,
|
||
随机中间值 ENUM_LAST,
|
||
random choice
|
||
|
||
布尔/状态 true, false, isActive=true,
|
||
无效状态值(如需要) isActive=false
|
||
```
|
||
|
||
### 9.2 系统化边界测试实现
|
||
|
||
```java
|
||
/**
|
||
* 边界测试策略实现
|
||
*/
|
||
class BoundaryTestStrategy {
|
||
|
||
/**
|
||
* 1. 数值边界测试
|
||
*/
|
||
@ParameterizedTest
|
||
@CsvSource({
|
||
"-1, false", // 低于最小值
|
||
"0, true", // 最小值
|
||
"1, true", // 最小值+1
|
||
"99, true", // 最大值-1
|
||
"100, true", // 最大值
|
||
"101, false", // 超过最大值
|
||
"2147483647, false", // Integer.MAX_VALUE (溢出风险)
|
||
})
|
||
void shouldValidateRange_forParticipantCount(int count, boolean expectedValid) {
|
||
ActivityRequest request = ActivityRequest.builder()
|
||
.participantCount(count)
|
||
.build();
|
||
|
||
Set<ConstraintViolation<ActivityRequest>> violations =
|
||
validator.validate(request);
|
||
|
||
boolean hasViolation = violations.stream()
|
||
.anyMatch(v -> v.getPropertyPath().toString().equals("participantCount"));
|
||
|
||
assertEquals(!expectedValid, hasViolation,
|
||
"参与者数量 " + count + " 应该" + (expectedValid ? "有效" : "无效"));
|
||
}
|
||
|
||
/**
|
||
* 2. 字符串边界测试
|
||
*/
|
||
@ParameterizedTest
|
||
@ValueSource(strings = {
|
||
"", // 空字符串
|
||
"a", // 1字符
|
||
"ab", // 2字符
|
||
"a".repeat(254), // 最大长度-1
|
||
"a".repeat(255), // 最大长度
|
||
"a".repeat(256), // 最大长度+1
|
||
" ", // 仅空白字符
|
||
"test@example.com", // 有效格式
|
||
"test", // 无效格式(无@)
|
||
"test@", // 无效格式(无domain)
|
||
"@example.com", // 无效格式(无local)
|
||
})
|
||
void shouldValidateEmail_forVariousInputs(String email) {
|
||
UserRequest request = UserRequest.builder()
|
||
.email(email)
|
||
.build();
|
||
|
||
boolean isValid = email != null &&
|
||
email.length() >= 5 &&
|
||
email.length() <= 255 &&
|
||
email.contains("@") &&
|
||
email.indexOf("@") > 0 &&
|
||
email.indexOf("@") < email.length() - 1;
|
||
|
||
Set<ConstraintViolation<UserRequest>> violations =
|
||
validator.validate(request);
|
||
|
||
assertEquals(!isValid, !violations.isEmpty(),
|
||
"邮箱 '" + (email == null ? "null" : email.substring(0, Math.min(email.length(), 20))) +
|
||
"...' 验证结果应该匹配预期");
|
||
}
|
||
|
||
/**
|
||
* 3. 集合边界测试
|
||
*/
|
||
@Test
|
||
void shouldHandleCollectionBoundaries() {
|
||
// 空集合
|
||
assertThrows(IllegalArgumentException.class, () ->
|
||
batchProcessor.processBatch(Collections.emptyList())
|
||
);
|
||
|
||
// 单元素
|
||
List<Item> singleItem = Collections.singletonList(new Item("1"));
|
||
BatchResult result1 = batchProcessor.processBatch(singleItem);
|
||
assertEquals(1, result1.getProcessedCount());
|
||
|
||
// 最大容量
|
||
List<Item> maxItems = IntStream.range(0, 1000)
|
||
.mapToObj(i -> new Item(String.valueOf(i)))
|
||
.collect(Collectors.toList());
|
||
BatchResult resultMax = batchProcessor.processBatch(maxItems);
|
||
assertEquals(1000, resultMax.getProcessedCount());
|
||
|
||
// 超过最大容量
|
||
List<Item> overMaxItems = IntStream.range(0, 1001)
|
||
.mapToObj(i -> new Item(String.valueOf(i)))
|
||
.collect(Collectors.toList());
|
||
assertThrows(BatchSizeExceededException.class, () ->
|
||
batchProcessor.processBatch(overMaxItems)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 4. 时间边界测试
|
||
*/
|
||
@ParameterizedTest
|
||
@CsvSource({
|
||
"1970-01-01T00:00:00Z, true", // Epoch开始
|
||
"2024-02-29T12:00:00Z, true", // 闰年2月29日
|
||
"2023-02-29T12:00:00Z, false", // 非闰年2月29日(无效)
|
||
"2038-01-19T03:14:07Z, true", // Y2K38边界前
|
||
"2038-01-19T03:14:08Z, false", // Y2K38边界后(32位时间戳溢出)
|
||
"2100-01-01T00:00:00Z, true", // 远期日期
|
||
})
|
||
void shouldValidateDateTime_forBoundaryValues(
|
||
String dateTimeStr, boolean expectedValid) {
|
||
try {
|
||
Instant instant = Instant.parse(dateTimeStr);
|
||
boolean isValid = instant.isAfter(Instant.EPOCH) &&
|
||
instant.isBefore(Instant.parse("2100-01-01T00:00:00Z"));
|
||
|
||
assertEquals(expectedValid, isValid,
|
||
"日期时间 " + dateTimeStr + " 验证结果应该为 " + expectedValid);
|
||
} catch (DateTimeParseException e) {
|
||
assertFalse(expectedValid,
|
||
"无效的日期时间格式应该验证失败");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 5. null和特殊值测试
|
||
*/
|
||
@ParameterizedTest
|
||
@NullAndEmptySource
|
||
@ValueSource(strings = {" ", "\t", "\n", "null", "NULL", "undefined"})
|
||
void shouldHandleSpecialStringValues(String input) {
|
||
// 测试系统对各种特殊字符串值的处理
|
||
String sanitized = sanitizer.sanitize(input);
|
||
|
||
assertNotNull(sanitized, "不应该返回null");
|
||
assertFalse(sanitized.equals("null"), "不应该保留字符串'null'");
|
||
assertFalse(sanitized.equals("undefined"), "不应该保留字符串'undefined'");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 边界测试数据生成器
|
||
*/
|
||
class BoundaryTestDataGenerator {
|
||
|
||
static Stream<Arguments> provideIntegerBoundaries(int min, int max) {
|
||
return Stream.of(
|
||
Arguments.of(min - 1, false, "below-minimum"),
|
||
Arguments.of(min, true, "minimum"),
|
||
Arguments.of(min + 1, true, "minimum+1"),
|
||
Arguments.of((min + max) / 2, true, "middle"),
|
||
Arguments.of(max - 1, true, "maximum-1"),
|
||
Arguments.of(max, true, "maximum"),
|
||
Arguments.of(max + 1, false, "above-maximum")
|
||
);
|
||
}
|
||
|
||
static Stream<String> provideStringBoundaries(int maxLength) {
|
||
return Stream.of(
|
||
"", // 空
|
||
"a", // 1字符
|
||
"a".repeat(maxLength / 2), // 中等长度
|
||
"a".repeat(maxLength - 1), // 最大-1
|
||
"a".repeat(maxLength), // 最大
|
||
"a".repeat(maxLength + 1), // 最大+1
|
||
" ", // 仅空白
|
||
"\t\n\r", // 控制字符
|
||
"<script>alert('xss')</script>", // XSS尝试
|
||
"DROP TABLE users;--", // SQL注入尝试
|
||
"日本語テスト", // Unicode
|
||
"🔥🎉💯", // Emoji
|
||
new String(new char[10000]).replace('\0', 'a') // 超长
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 9.3 边界条件检查清单
|
||
|
||
- [ ] 是否测试了最小值和最大值?
|
||
- [ ] 是否测试了最小值-1和最大值+1?
|
||
- [ ] 是否测试了空值(null, 空串, 空集合)?
|
||
- [ ] 是否测试了0值(除零风险)?
|
||
- [ ] 是否测试了负数(如果适用)?
|
||
- [ ] 是否测试了极大值(溢出风险)?
|
||
- [ ] 是否测试了特殊字符和Unicode?
|
||
- [ ] 是否测试了日期边界(闰年、时区)?
|
||
- [ ] 是否使用了@ParameterizedTest系统化测试?
|
||
- [ ] 是否测试了并发情况下的边界?
|
||
|
||
---
|
||
|
||
## 10. 持续改进检查表
|
||
|
||
### 10.1 定期审查计划
|
||
|
||
```
|
||
频率 审查内容 负责人
|
||
─────────────────────────────────────────────────────────
|
||
每日 新失败的测试分析 开发团队
|
||
每周 测试运行时间趋势分析 技术负责人
|
||
每月 测试质量评分和覆盖率审查 QA团队
|
||
每季度 全量测试审查和重构计划 架构师团队
|
||
每年 测试策略评估和工具升级 技术委员会
|
||
```
|
||
|
||
### 10.2 测试健康度指标
|
||
|
||
```java
|
||
/**
|
||
* 测试健康度评估脚本
|
||
*/
|
||
class TestHealthMetrics {
|
||
|
||
/**
|
||
* 1. 计算测试脆弱性指数
|
||
*/
|
||
void calculateTestFragilityIndex() {
|
||
Map<String, Integer> metrics = new HashMap<>();
|
||
|
||
// 不稳定测试比例
|
||
int flakyTests = countFlakyTests();
|
||
int totalTests = countTotalTests();
|
||
double flakyRatio = (double) flakyTests / totalTests;
|
||
|
||
// 平均修复时间
|
||
double avgFixTime = calculateAverageFixTime();
|
||
|
||
// 失败频率
|
||
double failureFrequency = calculateFailureFrequency();
|
||
|
||
// 脆弱性指数 = 不稳定比例 * 0.4 + 修复时间因子 * 0.3 + 失败频率 * 0.3
|
||
double fragilityIndex = flakyRatio * 0.4 +
|
||
(avgFixTime / 24) * 0.3 +
|
||
failureFrequency * 0.3;
|
||
|
||
System.out.println("测试脆弱性指数: " + String.format("%.2f", fragilityIndex));
|
||
System.out.println(" - 不稳定测试: " + flakyTests + "/" + totalTests);
|
||
System.out.println(" - 平均修复时间: " + avgFixTime + "小时");
|
||
System.out.println(" - 失败频率: " + String.format("%.2f", failureFrequency));
|
||
}
|
||
|
||
/**
|
||
* 2. 测试ROI计算
|
||
*/
|
||
void calculateTestROI() {
|
||
// 投入
|
||
double developmentTime = 120; // 小时
|
||
double maintenanceTimePerMonth = 8; // 小时
|
||
double infrastructureCost = 500; // 月度基础设施成本
|
||
|
||
// 产出
|
||
int bugsPrevented = 15; // 月度预防的缺陷数
|
||
double avgBugCost = 20; // 平均缺陷修复成本(小时)
|
||
int regressionBugsPrevented = 5;
|
||
|
||
double totalInvestment = developmentTime +
|
||
(maintenanceTimePerMonth * 12) +
|
||
(infrastructureCost * 12 / 100); // 折算为小时
|
||
double totalReturn = (bugsPrevented * avgBugCost * 12) +
|
||
(regressionBugsPrevented * avgBugCost * 2 * 12); // 回归缺陷成本更高
|
||
|
||
double roi = (totalReturn - totalInvestment) / totalInvestment * 100;
|
||
System.out.println("测试ROI: " + String.format("%.1f%%", roi));
|
||
}
|
||
}
|
||
```
|
||
|
||
### 10.3 改进行动清单
|
||
|
||
#### 立即行动(本周)
|
||
|
||
- [ ] 识别并标记所有不稳定测试
|
||
- [ ] 删除或重构虚假断言测试
|
||
- [ ] 修复最近的测试失败
|
||
- [ ] 更新测试文档
|
||
|
||
#### 短期行动(本月)
|
||
|
||
- [ ] 实施测试命名规范检查
|
||
- [ ] 添加边界条件测试覆盖率
|
||
- [ ] 优化慢速测试(>1秒)
|
||
- [ ] 建立测试基线性能指标
|
||
- [ ] 审查Mock使用并修复过度Mock
|
||
|
||
#### 中期行动(本季度)
|
||
|
||
- [ ] 实施并行测试执行
|
||
- [ ] 建立测试质量评分流程
|
||
- [ ] 添加并发测试套件
|
||
- [ ] 实施测试数据工厂模式
|
||
- [ ] 建立性能测试基线
|
||
|
||
#### 长期行动(本年度)
|
||
|
||
- [ ] 评估并升级测试框架
|
||
- [ ] 实施契约测试
|
||
- [ ] 建立测试可视化仪表板
|
||
- [ ] 建立测试驱动开发文化
|
||
- [ ] 实施AI测试生成质量门禁
|
||
|
||
### 10.4 自动化质量门禁
|
||
|
||
```yaml
|
||
# .github/workflows/test-quality-gate.yml
|
||
name: Test Quality Gate
|
||
|
||
on: [push, pull_request]
|
||
|
||
jobs:
|
||
quality-gate:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v3
|
||
|
||
- name: Run Test Quality Checks
|
||
run: |
|
||
# 1. 检查虚假断言
|
||
FAKE_ASSERTIONS=$(grep -r "assertTrue(true)\|assertFalse(false)" src/test --include="*.java" | wc -l)
|
||
if [ $FAKE_ASSERTIONS -gt 0 ]; then
|
||
echo "❌ 发现 $FAKE_ASSERTIONS 个虚假断言"
|
||
exit 1
|
||
fi
|
||
|
||
# 2. 检查getter/setter测试
|
||
DTO_TESTS=$(grep -r "testGetter\|testSetter" src/test --include="*.java" | wc -l)
|
||
if [ $DTO_TESTS -gt 10 ]; then
|
||
echo "❌ 发现 $DTO_TESTS 个疑似DTO框架测试"
|
||
exit 1
|
||
fi
|
||
|
||
# 3. 检查测试覆盖率
|
||
mvn jacoco:report
|
||
COVERAGE=$(grep -o 'Total[^%]*%' target/site/jacoco/index.html | grep -o '[0-9]*%' | head -1)
|
||
echo "当前覆盖率: $COVERAGE"
|
||
|
||
# 4. 检查测试执行时间
|
||
SLOW_TESTS=$(mvn test 2>&1 | grep -c "Time elapsed.*> 1 sec")
|
||
if [ $SLOW_TESTS -gt 20 ]; then
|
||
echo "⚠️ 发现 $SLOW_TESTS 个慢速测试"
|
||
fi
|
||
|
||
echo "✅ 测试质量门禁通过"
|
||
```
|
||
|
||
### 10.5 测试改进度量表
|
||
|
||
| 指标 | 基线 | 目标 | 当前 | 差距 |
|
||
|------|------|------|------|------|
|
||
| 虚假断言数量 | 50 | 0 | ? | 待评估 |
|
||
| DTO框架测试数量 | 30 | 0 | ? | 待评估 |
|
||
| 平均测试执行时间 | 2.5s | <1s | ? | 待评估 |
|
||
| 不稳定测试比例 | 8% | <2% | ? | 待评估 |
|
||
| 边界条件覆盖率 | 45% | >80% | ? | 待评估 |
|
||
| 并发测试数量 | 0 | >20 | ? | 待评估 |
|
||
| 性能测试覆盖 | 0% | >50% | ? | 待评估 |
|
||
| 测试命名规范率 | 60% | >95% | ? | 待评估 |
|
||
|
||
---
|
||
|
||
## 附录A: 快速参考卡
|
||
|
||
### 测试反模式速查表
|
||
|
||
| 反模式 | 检测方法 | 修复建议 |
|
||
|--------|----------|----------|
|
||
| 虚假断言 | `grep -r "assertTrue(true)"` | 删除或添加有意义的验证 |
|
||
| 测试框架 | `grep -r "testGetter\|testSetter"` | 删除DTO/Entity的框架测试 |
|
||
| 过度Mock | 检查Mock核心业务类 | 使用真实对象或Spy |
|
||
| 硬编码 | 查找魔法数字 | 提取为命名常量 |
|
||
| 测试依赖 | 检查@Order注解 | 移除依赖,独立准备数据 |
|
||
| 缺少边界 | 检查参数化测试 | 添加@ParameterizedTest |
|
||
| 慢测试 | 执行时间>1秒 | 优化或使用@Tag("slow") |
|
||
| 无文档 | 检查注释和命名 | 添加Given-When-Then |
|
||
|
||
### 代码审查检查清单(复制到PR模板)
|
||
|
||
```markdown
|
||
## 测试代码审查检查单
|
||
|
||
### 质量
|
||
- [ ] 测试验证了有意义的业务逻辑(非框架代码)
|
||
- [ ] 断言具体且有意义(非assertTrue(true))
|
||
- [ ] 使用了组合断言assertAll验证多个条件
|
||
- [ ] 异常测试使用了assertThrows
|
||
|
||
### 边界
|
||
- [ ] 测试了null输入
|
||
- [ ] 测试了空集合/字符串
|
||
- [ ] 测试了最大值/最小值
|
||
- [ ] 测试了无效/异常输入
|
||
|
||
### 可维护性
|
||
- [ ] 测试名使用should_when格式
|
||
- [ ] 使用了Given-When-Then结构
|
||
- [ ] 无硬编码魔法值
|
||
- [ ] 重复的setup抽取为方法
|
||
|
||
### Mock
|
||
- [ ] 只Mock外部依赖
|
||
- [ ] 没有Mock核心业务逻辑
|
||
- [ ] 配置了具体的参数匹配
|
||
- [ ] 验证了重要的Mock交互
|
||
|
||
### 并发
|
||
- [ ] 并发场景使用了CountDownLatch
|
||
- [ ] 验证了不变量和最终一致性
|
||
- [ ] 考虑了竞态条件
|
||
- [ ] 测试可以并行执行
|
||
```
|
||
|
||
---
|
||
|
||
## 附录B: 推荐阅读
|
||
|
||
1. **《xUnit Test Patterns》** - Gerard Meszaros
|
||
2. **《Test Driven Development: By Example》** - Kent Beck
|
||
3. **《Growing Object-Oriented Software, Guided by Tests》** - Freeman & Pryce
|
||
4. **JUnit 5官方文档**: https://junit.org/junit5/docs/current/user-guide/
|
||
5. **Mockito最佳实践**: https://site.mockito.org/
|
||
|
||
---
|
||
|
||
## 版本历史
|
||
|
||
| 版本 | 日期 | 修改内容 | 作者 |
|
||
|------|------|----------|------|
|
||
| 1.0 | 2026-02-03 | 初始版本,基于1210个测试的经验总结 | AI Assistant |
|
||
|
||
---
|
||
|
||
**如何使用本文档:**
|
||
|
||
1. **新开发人员**: 阅读第1、6、8章,了解基本规范
|
||
2. **代码审查**: 使用附录A的检查清单
|
||
3. **重构测试**: 参考第3、7、9章的具体方法
|
||
4. **建立规范**: 实施第10章的持续改进流程
|
||
5. **团队培训**: 分享第1章的AI陷阱识别,避免生成低质量测试
|
||
|
||
---
|
||
|
||
*本文档基于蚊子项目真实测试代码的经验教训编制,旨在建立高质量、可维护的测试文化。*
|