Git 与 CI:接口回归、冲突修复与交接

课前先保留判断:课前预习;课后用你的结果复验:课后复习。

今天从哪里开始

两位开发者分别修改校验和错误响应,Git 没报冲突,但合起来的接口却答错了。今天从这个场景开始:合并文件只是第一步,合并后的业务行为还要重新验证。

先修一次冲突并跑回归,再让同伴按 README 从固定提交启动;最后把可重复的检查写入 CI,故意制造一次失败,读日志修复。

能力与项目增量

今天要学会什么

  • 分支与合并
  • 文本冲突与语义冲突
  • GitHub Actions 与 SHA
  • 测试分层与干净启动

课堂结束时,用自己的项目检查:

  • 能做出来:修复无文本冲突但业务规则错误的合并,并展示正常输入的结果。
  • 能查出问题:针对“本地绿与远端版本不同、README 隐藏步骤”中的风险,说明输入、预期与实际差异;失败后核对数据没有被误改。
  • 能解释修改:提交代码、测试和修改前后结果,讲清自己接受或拒绝了哪一处 AI 建议。

文本冲突解决,不等于语义冲突解决

文本冲突与业务冲突

设想两条分支:

  • 分支 A 把“已支付订单禁止取消”写入 Service(服务层);
  • 分支 B 修改 HTTP 错误适配,遇到非法状态时返回 409 和统一错误结构。

它们可能修改不同文件,Git 自动合并、没有任何文本冲突。但如果 B 使用了错误异常类型,最终系统仍然不符合合同。

反过来,两个人可能只在 README 同一行改字,Git 报文本冲突,但不影响业务代码。

因此合并后至少做三类检查:

  1. 文本检查:冲突标记是否全部消失;
  2. 合同检查:字段、状态、错误语义有没有被改坏;
  3. 运行检查:目标测试、全量测试与主链命令是否通过。
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 不是项目宣传作文。接收者需要按它完成一次真实旅程。

最低内容:

  1. 项目解决什么业务问题;
  2. 支持的 Python 与操作系统边界;
  3. 从远端仓库获取代码;
  4. 创建并激活虚拟环境;
  5. 安装依赖;
  6. 准备样本数据;
  7. 启动基础 API,查询商品并走一次订单创建与详情查询;
  8. 运行测试;
  9. 常见故障和定位入口;
  10. 当前未实现的范围。

命令必须可以复制运行,并明确在哪个目录执行。例如只写:

运行 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 从指定提交开始重新执行。

两者都要通过,但不能互相替代。


本地绿灯与远端 CI 绿灯如何对应?

比较固定 commit SHA、依赖和测试命令。CI 验证的是触发运行的那份提交,不是作者当前还没提交的代码。

CI 是什么

同伴能够重现以后,再让自动化执行同样的检查。先说明 CI 验证的是哪份提交,再读脚本,才能看懂一次绿灯究竟证明了什么。

CI 是 Continuous Integration,中文常说“持续集成”。

在本课程里,它至少完成:

  1. 获取指定 commit;
  2. 安装锁定依赖;
  3. 准备测试环境;
  4. 执行格式/静态/测试入口;
  5. 保存结果;
  6. 失败时阻止把问题当成可交付版本。

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。下周沿用这套业务服务与数据,依次处理同时下单、重复读取、操作权限和浏览器联调的问题。

官方资料