Python 项目如何在 CI 中做自动化测试?
简化版
Python 项目 CI 通常在每次提交或合并请求时自动安装依赖、运行格式检查、静态检查、单元测试、覆盖率统计和构建验证。CI 的目标是尽早发现问题,防止不合格代码进入主分支。
详细版
典型 CI 流程包括:
- 检出代码;
- 安装指定 Python 版本;
- 安装依赖;
- 运行格式检查,如 black、ruff;
- 运行类型检查,如 mypy;
- 运行 pytest;
- 生成覆盖率;
- 构建包或镜像;
- 上传测试报告或覆盖率报告。
示例命令:
ruff check .
black --check .
mypy app
pytest --cov=app --cov-fail-under=80
面试要强调:CI 不只是跑测试,它是代码质量门禁和团队协作流程的一部分。
完整版教学
一、CI 的核心价值
CI 是 Continuous Integration,持续集成。它的核心价值是让代码尽快合并、尽快验证、尽快暴露问题。
没有 CI 时,开发者可能在本地忘记跑测试,或者本地环境和别人不同,代码合并后才发现失败。CI 把验证流程放到统一环境中执行,让每次提交都有可重复的质量检查。
对于 Python 项目,CI 尤其重要,因为 Python 是动态语言,很多错误只有运行或静态检查时才会暴露,比如导入错误、拼写错误、类型不匹配、依赖缺失。
二、一个健康的 Python CI 应该跑什么
基础 CI 至少应该跑测试:
pytest
更完整的 CI 应该包含格式、静态检查、类型检查和覆盖率:
ruff check .
black --check .
mypy app
pytest --cov=app --cov-report=xml
如果项目是库,还应该构建包:
python -m build
如果项目是服务,还可以构建 Docker 镜像并做最小启动验证。
三、CI 为什么要固定 Python 版本和依赖
本地能跑不代表 CI 能跑,CI 能跑也不代表生产能跑。为了减少环境差异,CI 应明确 Python 版本,并使用锁定的依赖文件。
例如项目使用 Python 3.11,CI 就不要默认使用系统 Python。依赖也应该通过 lock 文件或固定版本安装,避免今天安装和明天安装得到不同依赖。
可以使用 pip-tools、Poetry、uv 等工具生成锁文件。这样 CI 安装依赖更稳定。
四、测试分层在 CI 中怎么安排
并不是所有测试都必须每次提交全量跑。常见策略是:
- 每次提交跑格式检查、静态检查、单元测试;
- 合并请求跑关键集成测试;
- 发布前跑完整集成测试和端到端测试;
- 定时任务跑慢测试和兼容性测试。
pytest 可以用 marker 分组:
@pytest.mark.slow
def test_large_import():
...
CI 中可以选择:
pytest -m "not slow"
这样能在反馈速度和测试完整性之间取得平衡。
五、CI 中如何处理数据库和外部服务
集成测试可能需要 PostgreSQL、Redis、消息队列。CI 中可以用服务容器、Docker Compose 或 Testcontainers 启动依赖。
关键原则是:CI 使用测试环境,不连接生产资源;测试数据可重复创建和清理;外部第三方服务尽量 mock 或使用沙箱环境。
敏感信息要通过 CI Secret 注入,不要写进代码或日志。测试失败时,也不要把 token、密码、连接串打印出来。
六、覆盖率门禁和报告
CI 可以设置覆盖率阈值:
pytest --cov=app --cov-fail-under=80
也可以上传 XML 报告到覆盖率平台。团队更应该关注新增代码覆盖率和核心模块覆盖率,而不是只看项目总覆盖率。
老项目如果历史覆盖率很低,可以先不强行全局高门槛,而是要求新增代码必须有测试,逐步提升质量。
七、常见误区与追问
| CI 阶段 | 常见命令 | 失败意义 |
|---|---|---|
| 格式检查 | black --check . | 风格未统一 |
| Lint | ruff check . | 存在未使用导入、潜在 bug 等 |
| 类型检查 | mypy app | 类型契约不一致 |
| 测试覆盖率 | pytest --cov=app --cov-fail-under=80 | 行为验证或覆盖率不足 |
提交代码 -> 安装锁定依赖 -> lint/type/test 并行跑 -> 上传报告 -> 门禁通过后允许合并
如果 CI 需要 20 分钟,团队很可能绕过;控制在几分钟内更健康。
记忆钩子:CI 是主分支门禁,不是“自动跑一下 pytest”的装饰。
第一个误区是 CI 只跑 pytest,不跑 lint 和类型检查。很多问题测试未必覆盖,但静态工具可以提前发现。
第二个误区是 CI 太慢。太慢的 CI 会让团队绕过它。应该缓存依赖、拆分任务、分层运行测试。
第三个误区是 CI 依赖真实外部服务。外部服务抖动会让测试假失败,团队逐渐不信任 CI。
第四个误区是允许失败的 CI 合并主分支。门禁没有强制力,就很难形成质量约束。
- 误区:CI 只跑 pytest 就完整了。 测试不一定覆盖所有路径,格式、lint、类型检查、构建验证能提前发现另一类问题。
- 误区:CI 越全越好,不用管速度。 CI 太慢会拖慢反馈甚至被绕过,应通过缓存依赖、拆分任务、分层测试来控制耗时。
- 误区:CI 可以连真实生产依赖。 CI 必须使用测试库、服务容器、沙箱或 mock,不能污染生产资源,也不能泄露密钥。
- 追问:为什么要锁定 Python 版本和依赖? CI 的价值是可重复验证,版本漂移会导致今天通过、明天失败或与生产不一致。
- 追问:慢测试怎么放进 CI? 可以用 pytest marker 分组,每次提交跑快速测试,发布前或定时任务跑慢集成和 E2E。
- 追问:CI 失败是否允许合并? 主分支门禁通常不应允许失败合并,否则质量约束会失去可信度。
八、加强记忆
记 Python CI:统一环境里自动跑格式、静态检查、类型检查、测试、覆盖率和构建。CI 的价值不是炫流程,而是把质量问题挡在主分支外,同时让团队对每次合并有信心。