主题
界面
主题
《CI 分层执行与报告归档》解决了"结果可追溯";本节解决**"为什么这次跑得通、下次跑不通"**:
本地跑通 → CI 挂了 → 看日志 → "Connection refused" ← 环境问题 ✗
第一次跑通 → 第二次挂了 → 数据重复了 ← 数据问题 ✗本节的两个主题:环境(跑到哪去)与数据(用什么数据、用完怎么还)✓
"在我机器上是好的"这句话背后,通常是四个不同的问题 ✗:
| 说法 | 真实原因 | 属于 |
|---|---|---|
| "我这里是好的" | CI 的 JDK 版本不同 | 环境一致性 ✗ |
| "我这里是好的" | 本地有上次留下的数据,CI 是干净的 | 数据准备 ✗ |
| "我这里是好的" | 本地跑第 2 次就不行了(数据重复) | 幂等性 ✗✗ |
| "我这里是好的" | 本地连的是测试库,CI 连的是另一个 | 配置外置 ✗ |
这四类问题的共同点:它们都不是"代码问题",但都表现为"测试失败" ✓
所以它们必须被单独治理 ✓✓
| 环境 | 用途 | 数据 | 谁在用 |
|---|---|---|---|
| 本地 | 开发自测 | 随便造 | 开发者 ✓ |
| CI ★★ | 每次提交验证 | 必须隔离、必须可重复 | 流水线 ✓ |
| 预发/测试环境 ★ | 集成、联调、UI、性能 | 接近生产但可污染 | 测试团队 ✓ |
★ 关键认识:
同一套测试代码,在不同环境上的"前置条件"可能完全不同 ✓
比如本项目的 UI 测试:本地要手工把应用起起来;CI 里应该由流水线负责启动 ✓
所以"测试代码"要能表达"我需要什么前置条件",而不是假设它天然存在 ✓✓
| 抓手 | 做什么 | 不做的后果 |
|---|---|---|
| ① 版本锁定 ★★★ | JDK / Maven / 依赖版本写死 | "昨天还好好的" ✗ |
| ② 容器化 ★★ | 用镜像固定 OS 与系统依赖 | 换台机器就挂 ✗ |
| ③ 配置外置 ★★★ | 连接串、端口、开关从环境读 | 连错库 ✗✗ |
| ④ 依赖隔离 ★★ | 测试用的 DB/缓存独立于开发环境 | 测试污染别人的数据 ✗✗ |
★ ① 的实操:
✗ 用 mvn(版本随机器)
✓ 用 ./mvnw(Maven Wrapper,版本锁定)★
✓ 用 maven-enforcer-plugin 强制 JDK 版本本项目就是这么做的(《Maven 基础》):labs 用 mvnw ✓
★ ③ 的实操:
# ✗ 写死
spring:
datasource:
url: jdbc:mysql://localhost:3306/test_db
# ✓ 从环境读,带默认值
spring:
datasource:
url: ${DB_URL:jdbc:h2:mem:test}★ ④ 的一句话原则:
测试可以"脏",但只能脏它自己的那一块 ✓✓
这张表决定了"测试能不能重复跑":
| 方式 | 做法 | 优点 | 代价 |
|---|---|---|---|
| ① 硬编码 ✗ | 写死"商品 ID = 1" | 简单 | 依赖既有数据,脆弱 ✗✗ |
| ② 数据工厂 ★★ | 代码造对象(Builder) | 可控、可读 | 需要维护工厂 ✓ |
| ③ API 自建 ★★★ | 用被测接口自己造数据 | 贴近真实、自带清理线索 | 慢一点 ✓ |
| ④ 快照/夹具 ★ | SQL 脚本导入固定数据 | 快、可复现 | 容易与真实数据漂移 ⚠️ |
★ ③ 是接口测试的首选(T2-006):
// 自建:用接口创建一条属于自己的数据
String orderNo = given().body(...).when().post("/api/orders")
.then().extract().jsonPath().getString("orderNo");
// 自清:用完删掉
given().when().delete("/api/orders/" + orderNo);它的三个好处:
| 好处 | 说明 |
|---|---|
| 不依赖既有数据 ✓ | 空库里也能跑 ✓ |
| 数据形态一定合法 ✓ | 因为是被测系统自己接受的 ✓ |
| 能重复跑 ✓ | 每次都新建、每次都删掉 ✓ |
★ 与 ① 硬编码的对比:
✗ 硬编码:断言 "订单 ORD-1 的商品是机械键盘" ← 别人改了这条数据,测试就挂了
✓ 自建自清:自己造 ORD-x,断言 "它的商品是我填的那个" ← 谁都不影响 ✓| 策略 | 做法 | 隔离度 | 成本 |
|---|---|---|---|
| 独立库 ★★★ | 每次跑用一个全新库 | 最强 ✓ | 建库慢 ⚠️ |
| 独立 Schema / 表前缀 ★★ | 同一个库里用不同 schema | 强 ✓ | 需要 DDL 支持 |
| 事务回滚 ★ | 测试包在事务里,结束回滚 | 中(不覆盖"跨请求"的场景) | 最快 ✓ |
| 逻辑隔离(唯一 ID)★ | 数据加唯一前缀,用完删 | 弱但够用 ✓ | 最低 ✓ |
★ 本项目的选择:逻辑隔离 ✓✓
唯一 productId:P-<UUID 前 8 位>
唯一地址:含时间戳
用完立即删除([T2-006](../api/T2-006) 的自建自清)为什么不用"事务回滚":
因为接口测试是"跨 HTTP 请求"的 ✗ ——每个请求是独立事务 ✓
所以"把测试包在事务里回滚"这个经典做法,对接口测试不适用 ✓✓(对"直接调 Service 的集成测试"才适用)
★ 这是一条容易被忽略的边界 ✓
"用完清理"的两条理由(缺一不可):
| 理由 | 说明 |
|---|---|
| ① 幂等 ★★★ | 不清理 → 第二次跑就冲突(唯一约束、数量断言)✗ |
| ② 不污染 ★★ | 别人跑测试时看到你留下的脏数据 ✗ |
★ 判据:"这套测试能不能连续跑两遍都通过?" ✓✓
如果能 → 数据治理合格 ✓ 如果不能 → 一定有"没清理"或"依赖了别人的数据" ✗
★ 另一件必须做的事:隐私 ⚠️
| 风险 | 对策 |
|---|---|
| 测试环境用了生产数据 ✗✗ | 脱敏(手机号、身份证、地址) |
| 报告/截图里带真实用户信息 ⚠️ | T2-017 5.3④ ✓ |
| 日志里打了完整请求体 ⚠️ | 《日志框架与日志规范》 的脱敏 ✓ |
这是一个真实的设计冲突 ✓ ——本项目就遇到了 ✓
背景:UI 测试需要一个运行中的应用 ✗ ——而应用可能没启动 ✗
两种做法:
| 做法 | 好处 | 坏处 |
|---|---|---|
| 直接失败 ✗ | 不会漏测 ✓ | "应用没起"和"页面坏了"都显示为红 ✗ ——分不清是环境问题还是缺陷 ✗ |
跳过(assumeTrue) ★ | 区分"环境未就绪"与"测试失败" ✓ | 可能悄悄跳过而没人发现 ✗✗(T2-017 5.2) |
★ 本项目的选择与配套措施:
@BeforeAll
static void checkAppReachable() {
appReachable = UiEnvironmentCheckTestSupport.canConnect(BASE_URL);
if (!appReachable) {
System.out.println("[UI 环境自检] 被测应用未启动(" + BASE_URL + "),本类将跳过。");
}
}
@BeforeEach
void setUp() {
assumeTrue(appReachable, "被测应用未启动,跳过 UI 测试");
}配套措施(关键):
| 措施 | 作用 |
|---|---|
ui-test.ps1 负责启动应用 ★★ | 把"前置条件"变成脚本的责任,而不是人的记忆 ✓✓ |
报告里看 tests 数 ★★★ | 跳过 ≠ 通过(T2-017 5.2)✓ |
| 跳过时打印原因 ★ | 日志里留一行,方便判断 ✓ |
★ 一条可以直接用的判断原则:
"跳过"可以用于"外部依赖未就绪",但绝不能用于"怕失败" ✓✓
区别在于:前者你能写出"怎么让它就绪"的脚本;后者你写不出来 ✓
String orderNo = create(); // 自建
try {
// 测试逻辑
} finally {
delete(orderNo); // 自清:放在 finally 里,失败也清
}★ finally 那一行很重要:测试失败时更要清理 ✓✓ ——否则脏数据会一直累积 ✓
String productId = "P-" + UUID.randomUUID().toString().substring(0, 8);★ 这也是压测参数化的做法(T2-013 5.1)✓ ——同一个思想:不要复用固定数据 ✓
assertEquals("机械键盘", getOrder("ORD-1").getProduct()); // ✗ 别人改了这条数据就挂本节没有新增代码——它总结的是本项目已经做过的三个决定 ✓
| 决定 | 做法 | 理由 |
|---|---|---|
| 用 H2 而不是真实 MySQL ★ | data-access 用 H2 的 MySQL 兼容模式 | 无需安装数据库即可复现 ✓ ——换真实库只改 URL 与驱动两处 ✓ |
| 逻辑隔离而非事务回滚 ★ | 唯一 ID + 自建自清 | 接口测试跨请求,事务回滚不适用 ✓(5.4) |
| 环境不可用则跳过(带配套) ★★ | assumeTrue + 脚本负责启动 + 报告看 tests | 区分"环境未就绪"与"测试失败" ✓ |
★ 第一条的代价要诚实说明:
H2 与 MySQL 的行为并不完全一致(《执行计划与慢查询定位》 实测过: H2 的优化器把
YEAR()与隐式类型转换都改写成了索引扫描,而 MySQL 上会失效)✓所以:功能类测试用 H2 没问题;索引与执行计划类测试必须在真实库上做 ✓✓
这条边界很重要:"用轻量替代品"能解决 80% 的场景,但要清楚剩下 20% 是什么 ✓
skipped 数 → 确认没有"因为环境问题悄悄跳过" ★★;| # | 错误 | 说明 |
|---|---|---|
| 1 | 依赖既有数据 ✗✗ | 空库跑不通(5.3①)✗ |
| 2 | 不清理 ✗✗ | 第二次跑就冲突(5.5)✗✗ |
| 3 | 清理写在测试末尾而不是 finally/@AfterEach ✗ | 失败时不清,脏数据累积 ✗ |
| 4 | 硬编码连接串 ✗✗ | 连错库(5.2③)✗ |
| 5 | 用生产数据做测试 ✗✗ | 隐私风险(5.5)✗✗ |
| 6 | assumeTrue 被用来"消音" ✗✗ | 测试悄悄消失(5.6)✗✗ |
| 7 | 以为"事务回滚"万能 ✗ | 接口测试跨请求,回滚不了 ✓(5.4) |
目标:让一套测试"连续跑两遍都通过"。
步骤:
@AfterEach 或 finally,再连跑两遍 ✓;skipped,确保没有"意外跳过" ★。验收方式:能说出"你的测试为什么能重复跑"。
扩展任务:给测试加"数据自检":
assumeTrue(env.isReady()) 包裹全部用例,报告显示 tests=30 skipped=30。有什么问题?怎么改?assumeTrue 兜底)✓;② 如果确实无法就绪,应该让流水线"失败"而不是"跳过" ✓;③ 至少要在报告汇总里把 skipped 显示出来 ✓failures 应为 0 ✓TB 测试专项 B 到此完成。后续:
暂无