拓冰建站拓冰建站
首页 / 资讯中心 / 正文

第39章:MySQL 的两大测试框架MTR、GUnit 与内核回归测试体系

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 --suiteinnodb——这是 InnoDB 的全部回归测试。如果改了 handler 层的代码——还要跑./mtr --suiteinnodb_undo,innodb_zip,parts。如果有测试失败——仔细看.reject文件和.result文件的 diff——判断是你的修改引起了预期的行为变化需要更新 .result——还是引入了 Bug需要修复代码。”技术映射MTR MySQL Test Run。.test文件定义测试 SQL 和操作.result文件定义预期输出。每次修改后跑 MTR——自动发现行为变化。小胖“那我的 PR 修改了一个函数——我怎么能知道哪些 MTR 测试可能会被影响不可能一次跑全部测试吧——那得几个小时。”大师“有几种办法缩小测试范围。第一——看你的改动在哪个目录。如果改的是storage/innobase/——就跑--suiteinnodb。如果改的是sql/——就跑--suitemain加上--do-testmain.select*。第二——用git diff看改了哪些文件——grep这些文件名在.test文件中的引用。如果.test文件中有--source include/xxx——说明它依赖了某个 include 文件——如果你的改动影响了那个 include 路径——也需要跑。第三——如果实在不确定——先跑 suiteinnodb——这是最大的套件——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 文件catt/show_slow_digest.testTESTEOF --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--suiteinnodb --big-test--parallel8# --big-test包含大数据量的慢测例# --parallel88 线程并行跑——加速# 复制相关测试./mtr--suiterpl--parallel4# 只跑已知失败的测试排查回归./mtr--suiteinnodb --do-test-listfailing_tests.txt# 跑上次失败的测试./mtr--suiteinnodb --retry-failure3--retry2# 生成测试报告./mtr--suiteinnodb --xml-reportinnodb_report.xml步骤四GUnit 单元测试——函数级验证// 文件unittest/gunit/my_gcd-t.cc// 步骤目标为第 37 章的 gcd UDF 编写 GUnit 测试#includegtest/gtest.h// 测试用例基本功能TEST(GcdTest,BasicCases){// 手动调用 gcd 算法与 UDF 中相同的实现autogcd[](longlong a,longlong b)-longlong{while(b!0){longlong tb;ba%b;at;}returna0?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 tb;ba%b;at;}returna0?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 tb;ba%b;at;}returna0?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\--suiteinnodb --do-testinnodb.innodb_bug*# 查看所有 InnoDB Bug 相关的回归测试# 5. 模拟一个 MTR 失败——修复——重新验证echoINSERT INTO non_existent VALUES (1);t/bad_test.test ./mtr--externsocket$HOME/mysql-install/mysql.sock t/bad_test21|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\--suiteinnodb--parallel4--xml-reportci_report.xml\--retry1--retry-failure0echoExit code:$?# 预期0全部通过4. 项目总结优点 缺点维度优点缺点/局限MTR .test/.result基于文本比对——简单直观可以覆盖 SQL 行为、错误码、崩溃恢复对时间/排序/平台差异敏感——需要 replace_column 等技巧GUnit函数级隔离——速度快——适合边界条件无法测试跨模块的集成行为并行执行--parallelN加速测试NCPU 核数某些测试不能并行使用同一资源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 实战修炼与源码剖析
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门