Git 与 CI:接口回归、冲突修复与交接
今天从哪里开始
两位开发者分别修改校验和错误响应,Git 没报冲突,但合起来的接口却答错了。今天从这个场景开始:合并文件只是第一步,合并后的业务行为还要重新验证。
先修一次冲突并跑回归,再让同伴按 README 从固定提交启动;最后把可重复的检查写入 CI,故意制造一次失败,读日志修复。
今天要学会什么
- 分支与合并
- 文本冲突与语义冲突
- GitHub Actions 与 SHA
- 测试分层与干净启动
课堂结束时,用自己的项目检查:
- 能做出来:修复无文本冲突但业务规则错误的合并,并展示正常输入的结果。
- 能查出问题:针对“本地绿与远端版本不同、README 隐藏步骤”中的风险,说明输入、预期与实际差异;失败后核对数据没有被误改。
- 能解释修改:提交代码、测试和修改前后结果,讲清自己接受或拒绝了哪一处 AI 建议。
文本冲突解决,不等于语义冲突解决
设想两条分支:
- 分支 A 把“已支付订单禁止取消”写入 Service(服务层);
- 分支 B 修改 HTTP 错误适配,遇到非法状态时返回 409 和统一错误结构。
它们可能修改不同文件,Git 自动合并、没有任何文本冲突。但如果 B 使用了错误异常类型,最终系统仍然不符合合同。
反过来,两个人可能只在 README 同一行改字,Git 报文本冲突,但不影响业务代码。
因此合并后至少做三类检查:
- 文本检查:冲突标记是否全部消失;
- 合同检查:字段、状态、错误语义有没有被改坏;
- 运行检查:目标测试、全量测试与主链命令是否通过。
git diff HEAD~1..HEAD
python -m pytest
python -m uvicorn app.foundation:app --host 127.0.0.1 --port 8001老师提问:“Git 显示 Already up to date,能证明业务正确吗?”不能。它只说明提交图关系,不说明测试和业务行为。
没有冲突提示,为什么仍需要重新走一遍订单业务?
Git 主要判断文本合并,无法保证业务规则一致。合并后复跑正常和错误路径,核对状态及返回合同,才能发现语义冲突。
Git 没有冲突标记就表示合并正确吗?
不表示。文本可合并但两处规则可能互相矛盾,这是语义冲突。需要合并后的测试和业务变化检查,不能以无标记作通关依据。
README 是可执行的交付说明
合并后的项目能在作者电脑运行,现在把它交出去。同伴需要的是一条能执行的路径:取代码、配环境、启动接口和核对结果,不能靠口头补步骤。
README 不是项目宣传作文。接收者需要按它完成一次真实旅程。
最低内容:
- 项目解决什么业务问题;
- 支持的 Python 与操作系统边界;
- 从远端仓库获取代码;
- 创建并激活虚拟环境;
- 安装依赖;
- 准备样本数据;
- 启动基础 API,查询商品并走一次订单创建与详情查询;
- 运行测试;
- 常见故障和定位入口;
- 当前未实现的范围。
命令必须可以复制运行,并明确在哪个目录执行。例如只写:
运行 python cli.py接收者无法判断解释器、工作目录、数据文件和模块导入条件。
更可执行的写法:
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python -m pytest
python -m uvicorn app.foundation:app --host 127.0.0.1 --port 8001以上命令从后端目录执行;先按项目说明配置自己的 MySQL,再启动 app.foundation。服务启动会持续占用当前终端,另开终端或浏览器完成请求验证。
Windows 和 Linux 激活命令不同,应分开写,不要把一个平台的证据伪装成另一个平台已经通过。
同伴反馈怎样回到仓库
同伴说“运行不了”不是足够的缺陷报告。至少提供:
| 字段 | 示例 |
|---|---|
| 提交 | a1b2c3d |
| 环境 | CentOS 7、Python 3.x |
| 执行命令 | 原样粘贴 |
| 原始错误 | 保留第一处项目栈帧 |
| 期望 | README 声明的结果 |
| 实际 | 真实观察 |
| 尝试 | 已做的排查 |
作者修复后不能只回复“好了”。需要新提交,并由同伴在干净目录或明确更新后的目录复验。
本地通过不等于远端提交通过
本地测试使用当前工作区。
工作区可能包含:
- 未提交文件;
- 本机已有缓存;
- 手工创建的数据;
- 没写进依赖清单的包。
CI 从指定提交开始重新执行。
两者都要通过,但不能互相替代。
本地绿灯与远端 CI 绿灯如何对应?
比较固定 commit SHA、依赖和测试命令。CI 验证的是触发运行的那份提交,不是作者当前还没提交的代码。
CI 是什么
同伴能够重现以后,再让自动化执行同样的检查。先说明 CI 验证的是哪份提交,再读脚本,才能看懂一次绿灯究竟证明了什么。
CI 是 Continuous Integration,中文常说“持续集成”。
在本课程里,它至少完成:
- 获取指定 commit;
- 安装锁定依赖;
- 准备测试环境;
- 执行格式/静态/测试入口;
- 保存结果;
- 失败时阻止把问题当成可交付版本。
CI 不是云部署。
CD(持续交付或持续部署) 有两种常见口径:Continuous Delivery(持续交付)和 Continuous Deployment(持续部署)。持续交付把通过测试的 commit 变成可部署制品;持续部署还会把制品自动送入明确环境。
本周仓库没有冻结镜像仓库、云服务器、域名、生产密钥和回滚方案,所以当前绿色工作流只能证明 CI。即使再自动构建镜像,也只能写“生成了可追溯交付制品”,不能写“生产部署完成”。
先建立本地绿色基线
推送前运行代码仓 README(项目说明文档) 的全量回归。
记录:
- 命令;
- Python 版本;
- 测试数;
- 结果;
- 测试数据库;
- 当前 commit;
- 工作区是否干净。
本地红灯时不要靠 CI 帮你“试试看”。
先在本地定位。
测试分层
Service(服务层) 单元测试:
- 快;
- 聚焦规则;
- 可使用受控替身。
API 合同测试:
- 验证路径、Schema(数据结构约束)、状态码和 ErrorResponse(统一错误响应)。
MySQL 集成测试:
- 验证真实关系、约束、事务和 rollback(回滚)。
Container(运行容器或运行单元) smoke(最小冒烟验证):
- 验证打包后进程能启动并响应关键入口。
四类证据回答不同问题。
CI 为什么不能只跑导入检查?
导入成功不能证明金额、事务或越权边界正确。先跑有业务断言的单元/合同测试,再按需要配置 MySQL、Redis 等集成门;未配置的服务不能声称已测。
让回归检查真正发现商城运营错误
为什么学:D01 导入器和 D03 查询器已经能演示一次,但合并后仍可能把 0 元价格当合法、遗漏超时清理。今天我给本人前四天的代码写测试,不另抄一份完成版。
pytest.fixture 把准备和清理作为可复用步骤。测试函数写同名参数即可请求 fixture。tmp_path 为测试提供独立临时目录,避免测试相互覆盖个人数据;fixture 中 yield 前准备、后清理,复用昨天的上下文边界思路。
import pytest
@pytest.fixture
def product_csv(tmp_path):
path = tmp_path / "products.csv"
path.write_text("product_id,name,unit_price_yuan\nP1001,鼠标,129.00\n", encoding="utf-8")
yield path
def test_sample_is_isolated(product_csv):
assert product_csv.exists()这个小例只证明样本准备;我真正的业务测试必须调用本人 iter_products,核对金额、拒绝坏行和错误行号。不能以“文件存在”代替导入正确。
参数化让一条规则接受多种反例
@pytest.mark.parametrize("price", [...]) 会让测试对每个值单独执行,失败结果标明是哪种输入。商城单价的反例包括零、负数、三位小数和 NaN,不是多写几个相同的成功样本。
@pytest.mark.parametrize("price", ["0", "-1", "1.001", "NaN"])
def test_bad_price(product_csv, price):
product_csv.write_text(
f"product_id,name,unit_price_yuan\nP1001,鼠标,{price}\n", encoding="utf-8"
)
with pytest.raises(ValueError):
list(iter_products(product_csv))这是测试主体片段,需要从本人模块导入 iter_products。为什么必须 list/next/for?因为生成器的校验在消费时执行,只构造生成器不会碰到坏行。
替身只能证明它控制的边界
配送接口尚未接入时,我用 AsyncMock 提供确定的异步返回,测试自己怎样组合商品和配送结果;不能据此宣称真实物流成功。monkeypatch.setenv/delenv 让某个测试控制环境,结束后恢复,不要求修改全机配置。
import asyncio
from unittest.mock import AsyncMock
from commerce_ops.detail_query import build_detail
lookup = AsyncMock(side_effect=[{"name": "鼠标"}, {"days": 2}])
result = asyncio.run(build_detail("P1001", lookup))
assert result["delivery"]["days"] == 2
assert lookup.await_count == 2若把被测的 build_detail 本身替换掉,再断言它返回了预设结果,就没有检查自己的业务。要替换不稳定的外部边界,保留正在验证的函数真实执行。
我补日包 test_ops_student.py 的 fixture 和断言,从本人累计项目根运行:
python -m pytest test_ops_student.py -q首次必须明确停在 STUDENT_TODO;缺包、缺本人前一日模块不能当作有效红灯。随后人为放宽一次单价校验,确认至少一个测试变红,再恢复规则;测试一直绿反而需要检查断言是否真的触发。
这个本地回归与原有 HTTP 合同、MySQL 回滚、Docker 请求一同进入交接清单。CI 执行同一命令、对应同一提交;真实服务未运行的层明确标记待验,不能用 mock 数量充当联调覆盖率。
Fixture 不等于真实环境
Fixture(固定测试数据或测试装置) 是 pytest(Python测试框架) 中组织测试前置和清理的机制。
它可以:
- 创建测试数据;
- 建立隔离数据库;
- 提供 TestClient;
- 测试后恢复状态。
Fixture 通过不能证明:
- 个人 Docker 可用;
- MySQL Volume(数据卷) 持久;
- 浏览器真实请求成功。
证据边界要诚实。
CI 结果必须关联 commit SHA
SHA 是 Git 提交的唯一标识。
交付记录不能只写“CI 绿了”。
至少写:
commit: abc123...
workflow: test
result: passed
tests: ...如果你又改了代码但没推送,旧 CI 不能证明新工作区。
同伴接手时,怎样确认他验证的是你交付的那一版?
记录提交编号、依赖与命令,并让同伴从该提交干净启动。截图和口头说明应关联同一版本,不能混用未提交的本地修改。
一个最小 CI 工作流要回答什么
- 使用哪个操作系统;
- 使用哪个 Python;
- 依赖从哪里安装;
- MySQL 怎样提供;
- 环境变量怎样注入;
- Schema / seed 怎样准备;
- 运行哪条测试命令;
- 失败日志怎样回读。
今天不追求 YAML 技巧。
重点是执行环境可重复。
第一次接触仍然要能逐层读懂脚本。打开 Dockerfile 与 CI/CD 脚本阅读指南,对照仓库中的 .github/workflows/*.yml 找出:
on在什么事件触发;jobs里有哪些任务;runs-on使用什么 runner;steps为什么有uses和run两种写法;working-directory是否指向真实目录;- 哪一步失败会阻止后续动作;
- 脚本有没有 deploy job,还是只有 test job。
配套的 CI/CD 脚本示例 额外展示 needs: test 和带 commit SHA 的镜像归档。它是教学用 Continuous Delivery 示例,不是生产部署答案。
CI(持续集成)脚本至少要明确哪些执行步骤?
触发条件、运行环境、取回代码、安装依赖和执行测试;需要数据库的测试另配服务与配置。故意制造失败检查是否变红,再修复确认对应提交通过。
先故意制造一次 CI 失败
选择可控错误:
- 忘记提交一个模块;
- 依赖清单缺一个包;
- 测试环境变量名称写错;
- Schema 初始化漏一步。
观察 CI 原始日志。
修复后关联两个 commit:
- 失败 commit;
- 修复 commit。
这比一张绿色截图更能说明你会排错。
学生自己动手,AI 只能改你允许改的地方
我先保存当前可运行版本,再完成“修复无文本冲突但业务规则错误的合并”。让 AI 一次只修改负责该行为的文件;修改前预测正常与失败结果,检查差异,再运行回归。每人需要解释一处接受或拒绝的建议。
错误与边界怎样验收
今天重点检查:本地绿与远端版本不同、README 隐藏步骤。保留输入、预期、实际输出和修复后的结果,不能只贴全绿截图。未验证的外部依赖如实记为待验证。
留给下一阶段
本周得到可复现的基础订单接口,而不只是一组 SQL。下周沿用这套业务服务与数据,依次处理同时下单、重复读取、操作权限和浏览器联调的问题。