1. 项目背景
业务场景:某数据库内核团队在开发一个新功能——“SELECT 语句支持 SKIP LOCKED 语法以跳过已锁行”。开发完成后,自测了几个场景都正常工作,PR 被合并到主分支。3 天后,QA 发现一个严重 Bug——在某些条件下,SELECT ... FOR UPDATE SKIP LOCKED会跳过未锁的行——导致查询结果不完整。这时候距离发版只剩 2 天——开发紧急修复了问题——但因为没有为该功能写完备的 MTR 测试——修复后又引发了另一个边界条件的回归——SELECT ... FOR UPDATE(不带 SKIP LOCKED)的行为也被改变了。
结果——这次版本带着一个阻塞性 Bug 发布——3 个客户的生产环境受影响——公司付出了惨重的信誉代价。
痛点:没有自动化测试体系——内核开发就是"在钢丝上修改代码":
- 没有回归测试:修了一个 Bug——引入了另一个 Bug——全靠手动测试——覆盖率几乎为零。
- 测试结果不可复现:依赖环境(时区、字符集、数据量)——同样的测试在不同机器上结果不同。
- 不知道哪些测例被影响了:改了一个函数——不知道有多少 MTR 测试依赖它的行为。
- 提交没有 CI 门禁:没有 PR 合并前自动跑全套 MTR 和 GUnit——人工漏检直接进入主分支。
本章带你掌握 MySQL 的两大测试框架——MTR(端到端回归测试)和 GUnit(单元测试),并教你为一类 Bug 编写最小复现测例和回归防护。
2. 项目设计
【场景:小胖的 PR 被 QA 打了回来——“你这个改动破坏了 6 个已有功能”】
小胖:“大师,为什么我改了一个函数——row_search_mvcc中的一行逻辑——会影响到 6 个看起来完全无关的功能?”
大师:“因为row_search_mvcc是 InnoDB 最底层的行读取函数——所有SELECT、UPDATE、DELETE都经过它。你改了它——哪怕只改一个条件判断——所有依赖 MVCC 的特性都可能受影响——SELECT FOR UPDATE、SKIP LOCKED、NOWAIT、Read View 创建——它们都在这同一个调用链上。”
小白:“那怎么才能知道我的修改会影响哪些 MTR 测试?”
大师:“在提交 PR 之前——跑./mtr --suite=innodb——这是 InnoDB 的全部回归测试。如果改了 handler 层的代码——还要跑./mtr --suite=innodb_undo,innodb_zip,parts。如果有测试失败——仔细看.reject文件和.result文件的 diff——判断是你的修改引起了预期的行为变化(需要更新 .result)——还是引入了 Bug(需要修复代码)。”
技术映射:MTR = MySQL Test Run。.test文件定义测试 SQL 和操作,.result文件定义预期输出。每次修改后跑 MTR——自动发现行为变化。
小胖:“那我的 PR 修改了一个函数——我怎么能知道哪些 MTR 测试可能会被影响?不可能一次跑全部测试吧——那得几个小时。”
大师:“有几种办法缩小测试范围。第一——看你的改动在哪个目录。如果改的是storage/innobase/——就跑--suite=innodb。如果改的是sql/——就跑--suite=main加上--do-test=main.select*。第二——用git diff看改了哪些文件——grep这些文件名在.test文件中的引用。如果.test文件中有--source include/xxx——说明它依赖了某个 include 文件——如果你的改动影响了那个 include 路径——也需要跑。第三——如果实在不确定——先跑 suite=innodb——这是最大的套件——2-4 小时能跑完——作为安全网。”
小白:“GUnit 和 MTR 有什么区别?好像两者都是测试——为什么需要两套?”
大师:“GUnit 是单元测试——测试单个函数或类——不启动完整的 mysqld。它快(几百个测试 1-2 分钟跑完)、隔离好(各测例不互相影响)。但它不能测试需要完整 MySQL 环境的行为(如 SQL 解析、执行计划、事务提交)。MTR 是端到端测试——启动一个完整的 mysqld 实例——执行 SQL——验证结果。它覆盖真实场景——但慢(需要启停实例)且测例之间可能互相影响。两者是互补的——GUnit 保证’零件合格’,MTR 保证’整车能开’。”
技术映射:GUnit = 单元测试(函数级,快,隔离),MTR = 集成测试(实例级,慢,真实)。开发流程 = 写代码 → 跑 GUnit → 跑最小 MTR → 跑完整套件。"
3. 项目实战
3.1 环境准备
# 确认 MTR 目录ls~/mysql-src/mysql-test/# MTR 关键目录结构:# mysql-test/t/ — 测试用例(.test 文件)# mysql-test/r/ — 预期结果(.result 文件)# mysql-test/suite/ — 套件测试(按模块组织)# suite/innodb/ — InnoDB 相关测试# suite/rpl/ — 复制测试# suite/group_replication/ — MGR 测试# GUnit 目录ls~/mysql-src/unittest/gunit/# 确认 MTR 可执行文件~/mysql-src/build-debug/runtime_output_directory/mysql-test-run.pl--version3.2 分步实现
步骤一:运行一个现有 MTR 测试——理解 .test / .result 机制
# 步骤目标:运行 main.select 测试——理解 MTR 的输出格式cd~/mysql-src/mysql-test# 运行单个测试./mtr--externsocket=$HOME/mysql-install/mysql.sock main.select# 输出示例:# Logging: ./mtr main.select# main.select [ pass ] 1234ms# ------------------------------------------------------------# All 1 tests were successful.# 查看 .test 文件内容cat~/mysql-src/mysql-test/t/select.test|head-30# 包含 SQL 语句和特殊命令:# -- 以 # 开头的行为注释(不执行)# SELECT 1;# --error ER_PARSE_ERROR ← 期望下一条 SQL 报指定错误# SELECT * FROM non_existent;# 查看 .result 文件内容head-30~/mysql-src/mysql-test/r/select.result# 包含 SQL 语句和预期输出——和 mysql 客户端的 output 格式相同步骤二:编写一个最小 MTR 测试——为自定义功能创建回归测试
# ============================================================# 步骤目标:为第 32 章的 SHOW SLOW DIGEST 命令编写 MTR 测试# ============================================================cd~/mysql-src/mysql-test# 1. 创建 .test 文件cat>t/show_slow_digest.test<<'TESTEOF' --echo # Test SHOW SLOW DIGEST command # 确保 Performance Schema 已启用 --source include/have_perfschema.inc # 执行一些慢查询以填充数据 CREATE TABLE t1 (id INT PRIMARY KEY); INSERT INTO t1 VALUES (1), (2), (3); SELECT SLEEP(0.5) FROM t1; # 执行 SHOW SLOW DIGEST(自定义命令) SHOW SLOW DIGEST; # 清理 DROP TABLE t1; TESTEOF# 2. 生成预期结果文件./mtr--recordt/show_slow_digest# 首次使用 --record——MTR 会生成 .result 文件并记录首次的输出作为基准# 3. 再次运行——验证通过./mtr t/show_slow_digest# 预期:[ pass ]# 4. 查看生成的 .result 文件catr/show_slow_digest.result步骤三:运行完整的测试套件——理解套件组织
# 步骤目标:跑套件级测试——理解哪些测试和你的改动相关# InnoDB 全套测试(约 600+ 个测试)./mtr--suite=innodb --big-test--parallel=8# --big-test:包含大数据量的慢测例# --parallel=8:8 线程并行跑——加速# 复制相关测试./mtr--suite=rpl--parallel=4# 只跑已知失败的测试(排查回归)./mtr--suite=innodb --do-test-list=failing_tests.txt# 跑上次失败的测试./mtr--suite=innodb --retry-failure=3--retry=2# 生成测试报告./mtr--suite=innodb --xml-report=innodb_report.xml步骤四:GUnit 单元测试——函数级验证
// 文件:unittest/gunit/my_gcd-t.cc// 步骤目标:为第 37 章的 gcd UDF 编写 GUnit 测试#include<gtest/gtest.h>// 测试用例:基本功能TEST(GcdTest,BasicCases){// 手动调用 gcd 算法(与 UDF 中相同的实现)autogcd=[](longlong a,longlong b)->longlong{while(b!=0){longlong t=b;b=a%b;a=t;}returna>=0?a:-a;};EXPECT_EQ(gcd(48,18),6);EXPECT_EQ(gcd(1071,462),21);EXPECT_EQ(gcd(17,13),1);EXPECT_EQ(gcd(0,5),5);EXPECT_EQ(gcd(5,0),5);}// 测试用例:边界条件TEST(GcdTest,EdgeCases){autogcd=[](longlong a,longlong b)->longlong{while(b!=0){longlong t=b;b=a%b;a=t;}returna>=0?a:-a;};EXPECT_EQ(gcd(1,1),1);EXPECT_EQ(gcd(1000000007,1000000009),1);EXPECT_EQ(gcd(-48,18),6);// 负数测试EXPECT_EQ(gcd(48,-18),6);}// 测试用例:大数TEST(GcdTest,LargeNumbers){autogcd=[](longlong a,longlong b)->longlong{while(b!=0){longlong t=b;b=a%b;a=t;}returna>=0?a:-a;};EXPECT_EQ(gcd(1234567890,987654321),9);}# 编译并运行 GUnit 测试cd~/mysql-src/build-debug cmake--build.-j$(nproc)--targetmy_gcd-t# 运行测试./unittest/gunit/my_gcd-t# 输出:# [==========] Running 3 tests from 1 test suite.# [ PASSED ] 3 tests.步骤五:Flaky 测试处理——时间敏感和排序敏感的测例
# 步骤目标:处理不稳定的测试(flaky tests)# Flaky 测试的常见原因:# 1. 时间敏感:SELECT NOW() 的结果每次都不同# 2. 排序不可靠:没有 ORDER BY 的 SELECT 顺序不确定# 3. 并发竞争:并发测试的线程启动顺序不确定# 在 .test 文件中处理时间敏感:# --replace_column 1 CURRENT_TIMESTAMP → 用固定字符串替换时间列# SELECT * FROM t1;# -- 在 .result 中——时间列被替换后稳定# 处理排序:# SELECT * FROM t1 ORDER BY id; → 加 ORDER BY 确保顺序# 标记已知的 flaky test:# 在 disabled.def 文件中添加:# main.flaky_test : BUG#12345 2026-05-01 YourName Test is unstable3.3 测试验证
# 验证清单# 1. MTR 自检——验证测试框架可用cd~/mysql-src/mysql-test ./mtr--externsocket=$HOME/mysql-install/mysql.sock main.simple# 预期:[ pass ]# 2. 验证自定义 .test 通过./mtr--externsocket=$HOME/mysql-install/mysql.sock t/show_slow_digest# 预期:[ pass ]# 3. GUnit 自检~/mysql-src/build-debug/unittest/gunit/thd-t--gtest_list_tests# 预期:列出 THD 相关的测试用例# 4. 检查 regression test 覆盖./mtr--externsocket=$HOME/mysql-install/mysql.sock\--suite=innodb --do-test=innodb.innodb_bug*# 查看所有 InnoDB Bug 相关的回归测试# 5. 模拟一个 MTR 失败——修复——重新验证echo"INSERT INTO non_existent VALUES (1);">t/bad_test.test ./mtr--externsocket=$HOME/mysql-install/mysql.sock t/bad_test2>&1|tail-5# 预期:[ fail ] ——学习如何读失败日志rmt/bad_test.test r/bad_test.result2>/dev/null# 6. CI 集成测试——模拟 Jenkins/GitHub Actions 调用./mtr--externsocket=$HOME/mysql-install/mysql.sock\--suite=innodb--parallel=4--xml-report=ci_report.xml\--retry=1--retry-failure=0echo"Exit code:$?"# 预期:0(全部通过)4. 项目总结
优点 & 缺点
| 维度 | 优点 | 缺点/局限 |
|---|---|---|
| MTR .test/.result | 基于文本比对——简单直观;可以覆盖 SQL 行为、错误码、崩溃恢复 | 对时间/排序/平台差异敏感——需要 replace_column 等技巧 |
| GUnit | 函数级隔离——速度快——适合边界条件 | 无法测试跨模块的集成行为 |
| 并行执行 | --parallel=N加速测试(N=CPU 核数) | 某些测试不能并行(使用同一资源) |
| CI 集成 | MTR 返回 exit code = 失败数——直接集成 Jenkins/GitHub Actions | 全量测试耗时长(InnoDB 全套 2-4 小时) |
适用场景
- 内核 Bug 修复:先写一个复现 Bug 的 .test → 修复代码 → 验证 .test 通过。
- 新功能开发:写完新功能后——立即编写 MTR 测试和 GUnit 测试。
- 版本升级验证:在新版本上跑老版本的 MTR——发现行为变化。
- PR 门禁:CI 中配置"至少跑 InnoDB 核心测试"——未通过不允许合并。
- 遗留 Bug 回归防护:每个已修复的 Bug 对应一个以 Bug 编号命名的 .test——防止复燃。
不适用场景:
- 性能压测:MTR 是功能测试——不测量性能——性能用 sysbench。
- UI 测试:MTR 只测试 MySQL 命令行协议——不涉及 GUI。
注意事项
--extern模式连接到已运行的 MySQL 实例:测试可能在数据上留下痕迹——生产环境勿用。.result文件应提交到版本控制:和.test文件一一对应——两者都是一等公民。- MTR 的
--record模式会覆盖.result:使用前确认行为变化是预期的——否则你是用 Bug 结果覆盖了正确结果。
常见踩坑经验
故障案例一:测试在自己机器上通过——CI 上失败——差异是时区。
根因:.result中的时间字段依赖服务器时区。
修复:测试中显式设置SET time_zone = '+00:00';或使用--replace_column替换时间列。故障案例二:
./mtr --record之后提交——发现大量 .result 文件被意外修改。
根因:--record模式会把所有相关测试的 .result 都更新(不限于你的改动)。
修复:./mtr --record t/my_test只对指定的测试录 result。故障案例三:排除了 flaky test——但它在 disabled.def 中躺了 2 年没人修。
根因:disabled 机制缺乏定期 review——轮为"垃圾桶"。
修复:disabled.def 中的每个条目必须标记过期日期——过期自动 re-enable——逼团队修复或重新评估。
思考题
- MTR 的
.test文件中--error ER_PARSE_ERROR指令为什么需要?如果不写——期待一条 SQL 报错——但实际没报错——MTR 会怎么处理? - 如果一个 GUnit 测试需要访问 InnoDB 的数据页——可以直接
new出一个 handler 对象来操作吗?为什么?
答案提示:第 1 题——MTR 默认任何 SQL 返回非 0 状态就算失败,
--error告诉 MTR “这条 SQL 预期返回这个错误码——如果没返回才报错”;第 2 题——不能(handler 需要 TABLE 对象和打开的 TABLE_SHARE——它们依赖完整的 MySQL 运行环境),GUnit 不适合测试存储引擎层——应使用 MTR 或 innodb 专用的单元测试框架。
延伸阅读与资源
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析