2519 words
13 minutes
TDD Guide
2026-04-11

TDD 学习整理#

1. 什么是 TDD#

TDD 是 Test-Driven Development,中文通常叫测试驱动开发。

它的核心思想不是先写业务代码,而是先写测试,再写实现,最后重构。
本质上是用测试来定义行为,用测试来驱动设计。

传统开发流程通常是:

  • 先写功能
  • 再手动测试
  • 最后补测试

TDD 的流程则是:

  • 先定义系统应该如何表现
  • 再写最少代码让测试通过
  • 最后重构代码结构

2. TDD 的经典流程#

TDD 的核心循环是:

  • Red
  • Green
  • Refactor

2.1 Red#

先写一个失败的测试。

这一步的重点是:先描述“系统应该怎么工作”,此时功能一般还没有实现,所以测试一定失败。

2.2 Green#

写最少的代码让测试通过。

这里不追求一次性写完全部逻辑,只要求当前测试通过即可。

2.3 Refactor#

在测试全部通过的前提下,对代码进行重构。

可以优化命名、拆分方法、消除重复、提升可读性。
因为测试已经兜底,所以重构会更安全。


3. TDD 的核心价值#

3.1 用测试驱动设计#

TDD 不只是验证工具,更是一种设计方式。
在编码前先思考输入、输出、边界条件和业务规则,通常会让代码结构更清晰。

3.2 降低 Bug 风险#

每个功能都先通过测试定义,再通过实现落地,可以减少遗漏和回归问题。

3.3 提升重构安全性#

测试是安全网。
当代码需要调整结构、优化逻辑时,只要测试仍然通过,就说明外部行为没有被破坏。

3.4 让需求更明确#

测试本质上是在写“可执行的需求说明”。
这会逼迫开发者在开始写代码前先把行为定义清楚。


4. 哪些实现适合使用 TDD#

没有绝对教条式标准,但有一个非常实用的判断原则:

适合 TDD 的实现,通常具有这些特征:

  • 输入明确
  • 输出明确
  • 规则明确
  • 可以快速验证
  • 不强依赖复杂外部环境

4.1 最适合 TDD 的场景#

纯业务逻辑#

例如:

  • 权限判断
  • 订单金额计算
  • 状态流转
  • 参数校验
  • 奖励函数评分逻辑

这类逻辑最适合 TDD,因为规则明确、断言简单、回归风险高。

工具类 / 转换函数 / 算法函数#

例如:

  • 字符串解析
  • 时间格式转换
  • 配置解析
  • JSON 转换
  • Prompt 模板拼装
  • 奖励值计算函数

Service 层核心规则#

例如:

  • 用户注册时用户名是否重复
  • 默认参数是否补齐
  • 用户能否访问某个任务
  • 某状态是否允许转移到下一状态

Bug 修复#

这是 TDD 非常经典的使用方式:

  • 先写测试复现 bug
  • 再修复代码
  • 最后保证测试通过

边界条件多的逻辑#

例如:

  • null 处理
  • 空列表
  • 非法输入
  • 极值情况
  • 并发场景下的状态变化

5. 哪些实现不适合强行使用 TDD#

5.1 UI 细节和复杂前端交互#

例如:

  • CSS 布局
  • 动画效果
  • 拖拽排序
  • 页面细节交互

这些更适合手工验证、E2E 测试或视觉回归测试。

5.2 强依赖外部系统的功能#

例如:

  • 真实调用第三方 API
  • 真实调用大模型接口
  • 真实 Redis / Kafka / 数据库联调
  • 真正执行 Python 训练脚本

这类内容往往更适合集成测试,而不是纯单元级 TDD。

5.3 探索性很强的原型代码#

如果需求和规则都还不稳定,例如:

  • Prompt 还在试
  • Agent 工作流还在探索
  • 奖励函数还在频繁改方向

这种阶段更适合快速原型验证,而不是一开始就做严格 TDD。

5.4 强依赖异步、线程、时序的复杂系统#

例如:

  • WebSocket 全链路
  • 分布式调度系统
  • 多进程训练联调
  • 主从切换与故障转移

这类场景不能只靠 TDD,需要结合集成测试和端到端测试。


6. 一个实用判断标准#

如果一个功能满足下面大部分条件,就适合 TDD:

  • 需求已经明确
  • 行为可以写成断言
  • 不依赖真实外部系统也能验证
  • 未来改动频繁
  • 容易出错
  • 重构风险高

如果一个功能具备以下特征,就不适合强上 TDD:

  • 主要是样板代码
  • 需求尚不清晰
  • 依赖复杂环境
  • 测试准备成本远高于实现成本
  • 反馈太慢,严重影响开发效率

7. 三问法:快速判断一个实现要不要用 TDD#

7.1 我能否先写出它应该怎么表现?#

如果能,说明适合 TDD。
如果连行为都说不清楚,说明还不适合。

7.2 我能否在不启动整个系统的情况下验证它?#

如果能,适合 TDD。
如果必须启动数据库、前端、消息队列、Python 进程,测试成本就比较高。

7.3 这个逻辑以后会不会频繁修改,且容易改坏?#

如果会,适合 TDD。
因为测试可以提供长期保护。


8. TDD 在 Spring Boot 项目中的适用位置#

8.1 很适合#

  • 工具类
  • Service 层规则逻辑
  • DTO 校验逻辑
  • 权限判断逻辑
  • 状态流转逻辑
  • Token 解析逻辑
  • 结果解析逻辑
  • Prompt 构造逻辑

8.2 一般适合#

  • Controller 层请求处理
  • WebSocket 推送前的数据封装
  • 参数绑定校验

8.3 不适合只靠 TDD#

  • WebSocket 全链路连接
  • ProcessBuilder 调 Python 的真实执行
  • 多进程训练联调
  • 分布式调度链路
  • 页面跳转与前端实时刷新体验

这些更适合集成测试、联调测试和端到端测试。


9. 结合 RL_Scheduler 项目的 TDD 拆分建议#

9.1 适合用 TDD 的模块#

  • AuthService 中的注册和登录规则
  • JWT 解析和权限判定逻辑
  • TrainingService 中的默认参数填充
  • 用户数据隔离规则
  • TrainingTask 状态流转逻辑
  • 奖励函数计算逻辑
  • 训练结果解析逻辑
  • 日志解析逻辑
  • Prompt 构造逻辑

9.2 更适合集成测试的模块#

  • WebSocket 实时推送全链路
  • Python 训练脚本的真实调度
  • 多进程训练执行
  • 分布式 Worker 调度
  • 前端页面交互和无刷新更新

10. TDD 最容易犯的错误#

10.1 什么都想用 TDD#

不是所有实现都适合 TDD。
强行使用只会拖慢进度。

10.2 对样板代码写过多测试#

例如纯 CRUD Controller,测试价值可能不高。

10.3 把环境问题误当成 TDD 问题#

TDD 更适合驱动逻辑,而不是直接驱动复杂环境。

10.4 测试过度依赖实现细节#

测试应该验证行为,而不是绑定具体内部实现。
否则一重构,测试就会大量失效。


11. 最实用的经验结论#

TDD 最适合优先用在以下三类实现上:

  • 高规则密度
  • 高出错风险
  • 高重构频率

如果一个模块同时满足这三点,就非常值得做 TDD。


12. 总结#

TDD 并不是“先写测试”这么简单,它本质上是一种开发方法和设计方式。

它最适合那些:

  • 规则明确
  • 输入输出清晰
  • 行为可断言
  • 不依赖复杂环境
  • 值得长期保护

的实现。

经典流程始终是:

  • 先写失败测试
  • 再写最少实现
  • 最后重构

也就是:

  • Red
  • Green
  • Refactor

一句话概括:

当你能先清楚定义“它应该怎么表现”,并且能快速验证这种表现时,这个实现通常就适合 TDD。

13. RL_Scheduler 中的 TDD 示例:训练任务默认参数补全 + 持久化 + 异步执行#

这个例子用 TrainingService.startTraining(request, userId) 来演示 TDD 的 Red → Green → Refactor。

13.1 需求(先定义“行为”)#

当用户提交训练任务时:

  • 如果 episodes == null,默认使用 1000
  • 如果 learningRate == null,默认使用 0.001
  • 创建任务时必须写入数据库(taskMapper.insert
  • 必须触发异步执行(trainingExecutor.executeTraining
  • 返回结果应为 status = PENDING

13.2 Red:先写一个失败的测试(JUnit 5 + Mockito)#

package com.example.demo.service;
import com.example.demo.dto.TrainingRequest;
import com.example.demo.dto.TrainingResult;
import com.example.demo.entity.TrainingTask;
import com.example.demo.mapper.TrainingTaskMapper;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.util.concurrent.CompletableFuture;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class TrainingServiceTddTest {
@Test
void startTraining_shouldFillDefaults_persistTask_andTriggerExecutor() {
TrainingTaskMapper taskMapper = mock(TrainingTaskMapper.class);
TrainingExecutor executor = mock(TrainingExecutor.class);
when(executor.executeTraining(anyString(), anyInt()))
.thenReturn(CompletableFuture.completedFuture(0.0));
TrainingService service = new TrainingService();
service.taskMapper = taskMapper;
service.trainingExecutor = executor;
TrainingRequest req = new TrainingRequest();
req.setAlgorithm("PPO");
req.setEpisodes(null);
req.setLearningRate(null);
Long userId = 1001L;
TrainingResult result = service.startTraining(req, userId);
assertNotNull(result.getTaskId());
assertEquals("PENDING", result.getStatus());
ArgumentCaptor<TrainingTask> captor = ArgumentCaptor.forClass(TrainingTask.class);
verify(taskMapper, times(1)).insert(captor.capture());
TrainingTask inserted = captor.getValue();
assertEquals("PPO", inserted.getAlgorithm());
assertEquals(1000, inserted.getEpisodes());
assertEquals(0.001, inserted.getLearningRate(), 1e-9);
assertEquals(userId, inserted.getUserId());
verify(executor, times(1)).executeTraining(result.getTaskId(), 1000);
}
}

此时如果你的 TrainingService.startTraining 还没实现这些行为,测试就会失败(Red)。


13.3 Green:写“最少代码”让测试通过#

(以下是思路示例:重点是“满足测试定义的行为”,而不是一次写完所有功能)

public TrainingResult startTraining(TrainingRequest request, Long userId) {
String taskId = UUID.randomUUID().toString().substring(0, 8);
int episodes = request.getEpisodes() != null ? request.getEpisodes() : 1000;
double learningRate = request.getLearningRate() != null ? request.getLearningRate() : 0.001;
TrainingTask task = new TrainingTask(taskId, request.getAlgorithm(), episodes, learningRate);
task.setUserId(userId);
taskMapper.insert(task);
trainingExecutor.executeTraining(taskId, episodes);
return new TrainingResult(taskId, "PENDING", 0.0, "任务已提交,请稍后查询状态");
}

当这段实现使测试全部通过时,就进入 Green。


13.4 Refactor:在测试保护下重构#

当测试通过后,可以安全重构,例如:

  • 抽出默认参数方法(可复用/可读性更好)
  • 把“构造 TrainingTask”单独封装成工厂方法
  • 统一状态常量(避免魔法字符串)

示例(仅展示重构方向):

private int resolveEpisodes(TrainingRequest request) {
return request.getEpisodes() != null ? request.getEpisodes() : 1000;
}
private double resolveLearningRate(TrainingRequest request) {
return request.getLearningRate() != null ? request.getLearningRate() : 0.001;
}

重构后再次运行测试仍然通过,就说明外部行为没有被破坏。


13.5 这个例子说明了什么#

  • 测试先定义了“正确行为”(默认值、入库、触发异步、返回 PENDING)
  • 实现只要满足测试即可,不需要一开始就写复杂逻辑
  • 重构时有测试兜底,改代码更安全
TDD Guide
https://sgjki547.top/posts/tdd_guide/
Author
SGJki
Published at
2026-04-11
License
CC BY-NC-SA 4.0