147 lines
4.1 KiB
Markdown
147 lines
4.1 KiB
Markdown
# 07 — 测试与错误处理规则
|
||
|
||
> **适用**:所有后端开发任务。AI 最容易忽略的两件事:写测试、统一错误码。
|
||
|
||
---
|
||
|
||
## 测试规则
|
||
|
||
### 必须写测试的场景
|
||
|
||
| 场景 | 测试类型 | 要求 |
|
||
|------|----------|------|
|
||
| 新增 Service 方法 | 单元测试 | 必须 |
|
||
| 新增 Controller 接口 | 集成测试 | 必须 |
|
||
| 涉及金额计算 | 单元测试 | 必须(多组边界值) |
|
||
| 涉及状态流转 | 单元测试 | 必须(覆盖所有状态路径) |
|
||
| 涉及外部接口调用 | 集成测试 | 必须(Mock 外部依赖) |
|
||
| 修 Bug | 回归测试 | 必须(防止复现) |
|
||
| 简单 CRUD | 可选 | 不强制 |
|
||
|
||
### AI 测试规则
|
||
|
||
**AI 完成任务后,必须同时交付测试代码。** 禁止说"功能写好了但测试你自己补"。
|
||
|
||
```java
|
||
// ✅ AI 必须产出完整测试
|
||
@SpringBootTest
|
||
class TaskServiceTest {
|
||
@Autowired
|
||
private TaskService taskService;
|
||
|
||
@Test
|
||
void shouldCreateTask() { ... }
|
||
|
||
@Test
|
||
void shouldNotCreateTaskWithoutTitle() { ... }
|
||
|
||
@Test
|
||
void shouldCalculateRewardCorrectly() { ... }
|
||
}
|
||
|
||
// ❌ AI 不能这样
|
||
// "功能已完成,测试代码未编写,建议后续补充"
|
||
```
|
||
|
||
### 测试命名
|
||
|
||
```
|
||
方法名_场景_预期结果
|
||
|
||
shouldCreateTask_whenValidInput
|
||
shouldNotCreateTask_whenTitleIsEmpty
|
||
shouldCalculateReward_whenDiscountApplied
|
||
shouldReturnFail_whenTaskNotFound
|
||
```
|
||
|
||
### 禁止
|
||
|
||
- 测试只测正常路径,不测异常路径
|
||
- 测试依赖执行顺序(每个测试必须独立)
|
||
- 测试里有 `Thread.sleep()`(用 Awaitility)
|
||
- 测试代码复制粘贴业务代码(测了个寂寞)
|
||
|
||
---
|
||
|
||
## 错误处理规则
|
||
|
||
### 返回格式(不变式)
|
||
|
||
```java
|
||
// ✅ 统一返回
|
||
Result.ok(data) // 成功
|
||
Result.error("原因") // 业务失败(用户可见)
|
||
Result.error("系统错误") // 系统异常(用户不可见,只记录日志)
|
||
|
||
// JSON 格式
|
||
{ "code": 200, "message": "success", "data": {...} } // 成功
|
||
{ "code": 500, "message": "任务不存在", "data": null } // 业务失败
|
||
{ "code": 500, "message": "系统繁忙,请稍后重试", "data": null } // 系统异常
|
||
```
|
||
|
||
### 什么情况返回什么
|
||
|
||
```java
|
||
// 查不到 → 业务失败
|
||
if (task == null) return Result.error("任务不存在");
|
||
|
||
// 参数不合法 → 业务失败
|
||
if (page < 1) return Result.error("页码必须大于0");
|
||
|
||
// 没权限 → 业务失败
|
||
if (!hasPermission) return Result.error("无权操作");
|
||
|
||
// 数据库连接失败 → 系统异常(全局异常处理器统一捕获)
|
||
// 第三方接口超时 → 系统异常(记录日志 + 返回通用错误)
|
||
// 不需要手动 try-catch 每个地方,用全局异常处理器统一处理
|
||
```
|
||
|
||
### 全局异常处理器
|
||
|
||
```java
|
||
@RestControllerAdvice
|
||
public class GlobalExceptionHandler {
|
||
|
||
@ExceptionHandler(BusinessException.class)
|
||
public Result<?> handleBusiness(BusinessException e) {
|
||
log.warn("业务异常: {}", e.getMessage());
|
||
return Result.error(e.getMessage()); // 用户可见
|
||
}
|
||
|
||
@ExceptionHandler(Exception.class)
|
||
public Result<?> handleException(Exception e) {
|
||
log.error("系统异常", e);
|
||
return Result.error("系统繁忙,请稍后重试"); // 用户不可见原文
|
||
}
|
||
}
|
||
```
|
||
|
||
### 禁止
|
||
|
||
| ❌ | ✅ |
|
||
|----|-----|
|
||
| `return Result.error(e.getMessage())` 把栈打印给用户 | `return Result.error("任务不存在")` |
|
||
| 每个方法都 try-catch 一遍 | 全局异常处理器统一处理 |
|
||
| 异常被吞掉 `catch(Exception e) {}` | 至少打日志 `log.error("xxx", e)` |
|
||
| `return null` 让前端爆 NPE | `return Result.error("原因")` |
|
||
| 成功和失败都用 code=200 | 失败用 code=500 |
|
||
|
||
---
|
||
|
||
## 检查表
|
||
|
||
### 测试
|
||
|
||
- [ ] 新增的非 CRUD 方法有测试
|
||
- [ ] 测试覆盖了异常路径(不是只测正常路径)
|
||
- [ ] 金额计算有边界值测试
|
||
- [ ] AI 交付包含测试代码
|
||
|
||
### 错误处理
|
||
|
||
- [ ] 错误信息用户能看懂(不是技术术语)
|
||
- [ ] 系统异常不暴露内部信息给用户
|
||
- [ ] 查不到/null 有明确 fail 返回
|
||
- [ ] 没有空 catch 块
|
||
- [ ] 没有 `return null` 代替错误返回
|