本页目录

软工 I · Git 内部原理与测试

对标:Pro Git(Chacon)/ MIT Missing Semester / Berkeley CS169 | 前置:crypto-01(哈希)、os-03(内容寻址的伏笔) 软件工程线讲"把代码写好、协作好、可靠交付"的工程实践。这一页两块:Git 的内部原理(你天天用它,但它底下是一个优雅的内容寻址数据结构,看懂它 git 就再也不会"背命令")和测试(软件可靠性的支柱——测试的层级、什么值得测、TDD 的思想)。

学习层:一次绿色测试,究竟证明了哪一段历史?

1. 具体谜题:回归在提交图的哪一条边上?

某个 feature 分支把 topic 解析器改成了缓存读取;单元测试全绿,但部署后的数据库适配器在真实 schema 上失败。另一个提交修复了输入边界,却没有改变业务输出。先预测:

  1. 哪一种测试最早能提供针对该回归的证据?
  2. 改动一个 blob 的一个字节,旧 commit 的对象 id 能保持不变吗?
  3. “覆盖率很高”能否替代对关键边界和集成接缝的测试?

实验台展示一个固定 commit DAG、对象哈希和三层测试证据;提交预测前隐藏诊断结果。目标是把“代码变了”“历史可追溯”和“行为被验证”分成三件事,而不是把绿色勾号当成系统证明。

2. 最小模型:内容寻址 DAG 加行为证据账本

Git 对象账本可写为 commit → tree → blob,commit 还包含父指针;分支只是指向某个 commit 的名字。测试账本写成

\[ E=(\mathrm{commit\ id},\mathrm{changed\ paths},\mathrm{test\ layer},\mathrm{trace},\mathrm{result}). \]

哈希把内容和历史的变化传播到对象 id;测试则把一个行为契约与输入、环境和断言绑定。两者都提供证据,但哈希完整性不等于行为正确,测试通过也不等于历史没有被篡改。

3. 正式机制与不变量:可追溯不等于可证明

  • 内容寻址不变量:相同对象内容得到同一 id;内容、父 commit 或 tree 改变,依赖它们的 id 应变化。commit 的父闭包构成可追溯 DAG。
  • 测试证据不变量:回归测试必须在能复现旧错误的输入上断言行为;集成测试必须真正穿过接缝;E2E 证据要记录外部依赖和环境。
  • 层级分工:单元测试提供快速局部证据,集成测试验证模块契约,E2E 验证关键用户路径。测试选择由故障边界决定,不由金字塔图形机械决定。
  • 审查连接:PR diff、对象历史、测试命令和失败日志应能互相指向;“本地通过”若没有固定命令和 commit id,就不是可复核的交付证据。

测试的形式是对行为集合的采样,不是穷尽所有输入;Git 的形式是完整性链,不是语义审查器。成熟报告要同时给出改了什么、测了什么、没测什么。

4. 失败边界与迁移任务

哈希碰撞、错误的测试夹具、未覆盖的环境差异、非确定性测试和 flaky E2E 都会削弱证据;100% 覆盖率仍可能漏掉错误断言。实验中的 toy object id 只展示传播关系,不是密码学安全实现,也不替代真实 git cat-file、bisect 和 CI 日志。

迁移任务:为 Medusa 的一个已修复 bug 写出 (commit, input, expected behavior, test layer, command) 五元组;再用 git show 和一次回归测试把它交给另一位协作者复核。若测试只证明 mock,不要把它升级为生产数据库证据。

JavaScript 失效时的静态读法:先按故障边界选择最早且足够的测试层,再看对象 id 是否随内容传播;覆盖率不是行为证书。

固定变更 首要证据 可见历史事实 仍需声明的边界
解析器拒绝空 topic 单元/回归 parser.js blob 改变,commit id 传播 未证明真实数据库
ORM 字段与 schema 不一致 集成 feature commit 指向新 tree 单元 mock 不能代替接缝
登录后页面跳转错误 E2E merge commit 有两个父节点 需要固定浏览器/环境

1. Git 的心脏:内容寻址的对象数据库

重点图。Git 对象模型:commit→tree→blob 的内容寻址 DAG + 分支只是指针。

图 se-01.2重点图。Git 对象模型:commit→tree→blob 的内容寻址 DAG + 分支只是指针。

commit DAG + 分支指针 + 三方合并。

图 se-01.3commit DAG + 分支指针 + 三方合并。

大多数人把 git 当"背命令的黑魔法",其实它的核心是一个极简优雅的数据结构——理解它,所有命令都变得可推理。Git 本质是一个内容寻址的键值存储:

commit 是一个 DAG(有向无环图,🔗 algo 线):每个 commit 指向父 commit,分支合并产生多父——git 历史就是一张 commit 的 DAG,分支只是指向某个 commit 的可移动指针。

这一下讲清了所有命令:

读法:Git 不是"文件的快照序列"的模糊概念,是"内容寻址对象组成的 DAG + 可移动的分支指针"——记住这个模型,你就从"背命令"升级到"推理 git 在干什么"。这是本页最高价值的认知。

2. 分支模型与协作

你的实践(memory 里的教训):Medusa 有"生产 ahead origin/main""dev 声明完成后 arch 必 git verify""单 commit 用 git show"这些纪律——它们全建立在'git 历史是可信、可追溯的 DAG'这个本页原理上。

3. 测试:软件可靠性的支柱

测试金字塔:单元(多快)→集成→E2E(少慢脆)。

图 se-01.1测试金字塔:单元(多快)→集成→E2E(少慢脆)。

代码会变、人会犯错——测试是"改动后仍正确"的自动化保证。测试金字塔(从多到少、从快到慢):

层 测什么 特点
单元测试 单个函数/模块 快、多、精确定位——主体
集成测试 模块间协作(如后端 + 数据库) 中等、测接缝
端到端(E2E) 整个系统(浏览器点到底) 慢、少、脆、最接近真实

金字塔原则:底层多、顶层少——单元测试快而稳,E2E 慢而脆(别倒过来堆一堆 E2E)。

什么值得测:

4. TDD 与测试的哲学

测试驱动开发(TDD):先写测试(红)→ 写最少代码让它过(绿)→ 重构(重构时测试保驾)。价值不只在测试本身——先写测试逼你先想清楚"这个函数的契约是什么"(输入输出、边界),是一种设计工具。不必教条式全程 TDD,但"写代码前先想怎么验证它对"是好习惯。

可测试性 = 好设计的副产品:难测的代码往往是设计差的信号(耦合太紧、副作用太多、依赖太硬)。纯函数最好测(pl-02,同输入同输出)——这是"优先纯函数"又一个理由。依赖注入、mock 外部服务(数据库、LLM API)让单元测试快而独立。

对你(Medusa):memory 提到"dev UI 任务必须实机验收""pytest 46 全过"——测试是你和 GPT/OPS 协作的验收契约。预测层的 predict.py、迷你数据库等,都该有测试当"改动不破坏"的护栏。

5. 练习与要点

例 1(看 git 的心脏) 在一个仓库里 git cat-file -p HEAD 看 commit 对象、git cat-file -p <tree哈希> 看 tree——亲眼看到"commit 指向 tree、tree 指向 blob",git 的对象模型不再抽象。

例 2(分支只是指针) 创建分支、看 .git/refs/heads/ 下那个只含一行哈希的文件——理解"创建分支为什么瞬间完成"(只写 41 字节)。

例 3(写回归测试) 给一个修过的 bug 写一个测试(能复现旧 bug 的输入 → 断言现在正确)——体会"回归测试防 bug 复活",把 memory 里"bug 修了要防回归"的教训工程化。\(\blacksquare\)


下一页:软工 II——CI/CD 与代码质量:自动化的构建-测试-部署流水线,以及代码审查、静态分析、技术债这些让团队可持续的实践。