能运行的脚本与可维护的项目之间,差的不只是目录数量。可靠项目要让新环境可以重建,让测试证明关键行为,让构建产物来自干净源码,并让本地与 CI 执行同一组命令。
本文以 Python 3.14 为基线,使用 PyPA 标准定义 pyproject.toml,以 pytest 展示测试方法。具体工具可以替换,但项目边界、依赖语义和验证闭环应保持稳定。
1. 从一个可安装项目开始
推荐的基础结构:
| |
src 布局把可导入包与仓库根目录分开。这样测试更容易针对“已安装的项目”运行,不会因为当前目录恰好在 sys.path 中而导入一份部署时不存在的代码。
包名和发行项目名不是同一个概念:发行名可包含连字符,例如 temperature-service;导入包通常使用下划线,例如 temperature_service。
2. pyproject.toml 的三层职责
一个可构建项目的最小配置可以是:
| |
三类表不能混为一谈:
[build-system]告诉构建前端要安装哪个后端来生成发行包;[project]是会进入项目元数据的名称、版本、运行时依赖等标准字段;[tool.<name>]保存某个工具自己的配置,语义由该工具定义。
Hatchling、Setuptools、Flit 等构建后端都可以生成标准产物。这里选择 Hatchling 只是为了给出完整示例,不代表它是唯一标准。
当前示例的发行名规范化后是 temperature_service_example,与导入包 temperature_service 不同,因此显式告诉 Hatchling 从 src/temperature_service 构建 wheel。若省略这段后端专用配置,Hatchling 的默认名称推断无法找到要打包的目录。其他构建后端有各自的包发现规则,切换后端时要同步调整配置。
脚本入口指向的 src/temperature_service/cli.py 至少要提供对应函数:
| |
3. 运行时依赖、可选依赖和开发依赖
运行项目必需的库放在 project.dependencies:
| |
版本范围应表达已验证的兼容边界。库通常不应把所有传递依赖精确钉死,否则会妨碍宿主解析安全更新;应用为了可重复部署,则需要在单独的锁定结果中确定完整环境。
用户可选择的功能依赖使用 extras:
| |
开发、测试和文档依赖不应成为安装库时的运行时依赖。标准化的依赖组可写为:
| |
依赖组不会写入构建产物的项目依赖元数据。当前 pip 支持从项目根目录安装指定组:
| |
依赖组格式是标准,但不同工具的命令行能力和最低版本仍需核对。面向较旧工具链时,可继续使用 requirements 文件或工具自身的环境管理方案,并在项目文档中写明唯一入口。
4. 建立本地开发环境
从全新检出开始:
| |
macOS/Linux 把解释器路径换为 .venv/bin/python。可编辑安装让导入指向工作区源码,适合开发;发布验证还要安装并测试真正构建出来的 wheel。
不要把 .venv/、构建目录、缓存或覆盖率文件提交到 Git。应提交的是声明和锁定信息,使环境能够重建。
5. 第一个 pytest 测试
生产代码 src/temperature_service/domain.py:
| |
测试 tests/test_domain.py:
| |
脚本入口也要有最小行为测试。tests/test_cli.py:
| |
运行:
| |
测试名称应表达行为和条件,不要只写 test_1。浮点计算使用 pytest.approx() 表达容差语义,而不是对所有浮点数强行精确比较。
6. Fixture 管理准备与清理
Fixture 适合创建每个测试所需的上下文:
| |
tmp_path 为测试提供独立临时目录。yield 后可以写额外清理,但由 tmp_path 管理的目录通常无需手工删除。
Fixture 应按真实生命周期选择作用域。把可变数据库连接、缓存或容器盲目设为 session 级,可能让测试相互污染;每个测试都重建昂贵环境又会拖慢套件。优化前先确认隔离要求和耗时来源。
7. 测试替身与 patch 的边界
最稳定的测试替身来自显式依赖:
| |
测试可传入固定时钟,而不必修改全局时间函数。
确实需要修改环境变量或第三方调用时,pytest 的 monkeypatch 会在测试结束后恢复。假设 config.py 提供 load_settings,从环境变量读取超时设置:
| |
Patch 应作用于“被测模块实际查找的名称”,而不是机械修改原始定义位置。如果模块写了 from api import send,就 patch 该模块中的 send 引用。过多 patch 往往说明依赖隐藏在全局状态中,应优先改善设计。
8. 单元测试、集成测试与端到端测试
不同层级回答不同问题:
- 单元测试:一个函数或对象的规则是否正确;
- 集成测试:数据库、文件格式、消息系统或 HTTP 适配是否真正兼容;
- 端到端测试:从外部入口到关键结果的主链路是否可用。
Mock 成功只能证明“代码按模拟器设定运行”,不能证明真实数据库方言、网络超时或序列化格式正确。关键适配器要有集成测试,并在可控环境中验证真实依赖。
测试还应覆盖失败路径:非法输入、部分成功、超时、重试后重复副作用、取消以及清理失败。只覆盖顺利路径的高覆盖率仍可能给出虚假信心。
9. 覆盖率是发现盲区,不是质量分数
使用 coverage.py 记录分支覆盖:
| |
配置可以放在 pyproject.toml:
| |
阈值应防止明显倒退,而不是鼓励没有断言价值的测试。覆盖过的代码仍可能断言错误结果;未覆盖的异常分支也可能正是生产风险最高的部分。
10. 统一工具入口
pytest 的稳定 TOML 配置形式是:
| |
开发机和 CI 应运行相同命令:
| |
构建后还应在新虚拟环境安装 dist/ 中生成的 wheel,再运行至少一组导入和命令行冒烟测试。这能发现包数据遗漏、入口点错误和“只有可编辑安装能工作”的问题。
CI 至少固定:
- 支持的 Python 版本和操作系统矩阵;
- 安装依赖的声明或锁定输入;
- 测试、类型检查和构建命令;
- 缓存键中的 Python 与依赖文件版本;
- 产物来源的提交和构建日志。
不要因为依赖缓存命中就跳过依赖解析与环境验证,也不要让发布任务重新构建一份未经测试的不同产物。
11. 依赖锁定的准确边界
project.dependencies 表达项目运行所需的抽象范围,锁文件表达某些目标环境中解析出的具体版本,两者职责不同。
Python 生态已经标准化 pylock.toml(PEP 751)格式,但具体锁定、同步命令以及工具支持范围仍需查阅所选工具文档。项目可以使用标准锁文件,也可能因既有工具链保留专用锁文件;关键要求是:
- 锁文件由声明生成,不手工拼凑传递依赖;
- 应用部署使用受控、可重现的具体集合;
- 库发布保留合理兼容范围,不把开发环境全部精确版本强加给使用者;
- 自动化更新后重新执行测试和安全审查;
- 锁定不等于安全,仍需处理撤回版本、漏洞和包索引信任。
12. 小结
src布局让测试和构建更接近真实安装结果。pyproject.toml分别承载构建系统、项目元数据和工具配置,三者语义不同。- 运行时依赖、extras、开发依赖组和锁定结果解决不同问题。
- pytest 的参数化、Fixture 和 monkeypatch 应服务于行为验证与隔离,而不是堆砌技巧。
- 覆盖率只能暴露未执行区域,不能证明断言和需求正确。
- CI 应测试一次、构建一次,并发布那份已经验证的产物。
最后一篇将把项目带入性能分析和交付:如何先测量、再优化,并正确构建 wheel、部署应用或发布库。