← 代码课堂

第 12 课:工程化收尾 —— 测试、打包、发布

难度:★★☆(综合课,把前面 11 课串起来)
教材:resource_manager_v2/tests/(测试)+ worry_debate_game/build.bat、run.py(打包)+ 各项目的 README
预计时间:讲解 70 分钟 + 练习 60 分钟
学完你能:给项目写测试、打包成 exe、写清楚说明书,并把项目正式发布到 GitHub


课前须知:什么叫"工程化"

代码能跑,只是起点。工程化回答的是另外几个问题:

这些加起来,就是"一个项目从自己玩到别人能用"的完整闭环。


第一步:跑测试、打一次包

REM 1. 跑测试(看全绿)
cd resource_manager_v2
.venv\Scripts\pytest -v
REM 没建 venv:py -3 -m venv .venv && .venv\Scripts\pip install -e ".[dev]"

REM 2. 打包 exe(这个耗时较长,可选)
cd ..\..\01-应用项目\worry_debate_game
build.bat
REM 完成后 dist\worry_debate_game.exe 可以直接发给没装 Python 的人

观察重点:pytest -v 打印的每个 PASSED 背后,
都是"我保证这个功能现在是好的"——这就是工程化的安全感。


第二步:模块地图(工程化七件套)

一个"完整项目"通常包含:
├── ① 测试        tests/                自动验证功能
├── ② 依赖声明    requirements.txt / pyproject.toml
├── ③ 启动入口    run.py / start.bat    双击/一行命令就能跑
├── ④ 打包脚本    build.bat / *.spec    给没有环境的人用
├── ⑤ 文档        README.md / README.txt
├── ⑥ 版本管理    .git/ + .gitignore
└── ⑦ 发布        GitHub Release

对照教材项目:resource_manager_v2 有 ①②⑤⑥;worry_debate_game 有 ③④⑤⑥;
「查资料」有 ③⑤⑥。把这七件补齐,项目就"像个正经软件"了。


第三步:逐块精讲

块 1:测试 —— 给自己上保险(tests/ 三个文件)

先看最简单的模型测试(test_models.py):

class TestModels:
    def test_category_creation(self):
        cat = Category(name="Test")
        assert cat.name == "Test"
        assert cat.id is not None
        assert cat.parent_id is None

一条测试 = 准备 → 执行 → 断言。assert 就是"我认为结果应该是这样",
不成立就报错。

再看需要"背景环境"的测试(test_services.py 第 11-23 行):

@pytest.fixture
def setup(self):
    with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as f:
        db_path = f.name
    repo = SQLiteRepository(db_path)
    repo.connect()
    repo.init_schema()
    event_bus = EventBus()
    tree_service = TreeService(repo, event_bus)
    graph_service = GraphService(repo, event_bus)
    yield repo, tree_service, graph_service, event_bus
    repo.close()
    os.unlink(db_path)          # 测试结束后清理临时文件

这里用临时数据库——测试不会碰你的真实数据(隔离是测试的第一原则)

还有"测试预期会报错"的写法(test_io.py 第 100-105 行):

def test_registry_unknown_format(self):
    registry = get_registry()
    with pytest.raises(ValueError):
        registry.get_importer("csv")     # 不支持的格式,就应该抛 ValueError

pytest.raises 表示"我预期这段代码会抛这个异常"——
允许报错也是要测试的(错误处理也是功能)。

测试怎么分层? 看项目的三个测试文件:

文件测什么层次
test_models.py数据模型(不用数据库、不用界面)最底层、最快
test_repository.py数据库读写需要数据库
test_services.py业务组合(Service + Repository + EventBus)集成
test_io.py导入导出适配器功能模块

测试的价值:以后改代码,跑一遍 pytest,几秒就知道有没有改坏东西。
这就是为什么"敢重构"——因为测试兜底。你第 06 课敢做 v1→v2 重构,底气就来自这里。

块 2:依赖声明 —— 别人怎么装(复习 + 完整)

requirements.txt(「查资料」/「worry」风格,简单直接)
    fastapi>=0.115
    uvicorn>=0.30
# pyproject.toml(resource_manager_v2 风格,更正式)
[project]
dependencies = ["pyside6>=6.5.0", "pyyaml>=6.0"]

[project.optional-dependencies]
dev = ["pytest>=7.0"]

规则:

块 3:启动入口 —— 双击就能用

看 worry_debate_game/start.bat:

@echo off
cd /d "%~dp0"
set "PY=python"
py -3 --version >nul 2>nul && set "PY=py -3"
%PY% run.py
pause

会打开应用商店吗?这个探测就是为这种坑准备的)

再看 run.py 里处理打包后的路径:

if getattr(sys, 'frozen', False):
    base = sys._MEIPASS          # 打包后:临时解压目录
else:
    base = os.path.dirname(os.path.abspath(__file__))   # 源码运行
sys.path.insert(0, base)

sys.frozen 是"是否被打包成了 exe"的标志;打包后资源在 sys._MEIPASS。
这段代码让同一份代码既能在源码下跑,也能在 exe 里跑。 很典型的工程细节。

块 4:打包 exe —— 给没有 Python 的人用(build.bat)

py -3 -m PyInstaller --onefile --name "worry_debate_game" ^
  --add-data "worry_debate_game/static;worry_debate_game/static" ^
  --add-data "worry_debate_game/__init__.py;worry_debate_game" ^
  --hidden-import uvicorn.logging ^
  --hidden-import uvicorn.loops.auto ^
  --hidden-import uvicorn.protocols.http.auto ^
  --hidden-import langchain_openai ^
  --hidden-import langchain_core.messages run.py

参数逐个解释:

参数作用
--onefile打包成单个 exe(否则是一整个文件夹)
--name输出的名字
--add-data "源;目标"把非代码文件带进包里(静态网页、素材)。Windows 用 ; 分隔
--hidden-import xxx手动告诉 PyInstaller 还要带上某个模块(见下)

为什么需要 --hidden-import?
PyInstaller 靠"扫描 import 语句"找出要打包的模块。但 uvicorn 这类库会在运行时
用字符串动态加载模块(比如按配置决定用哪个协议实现),静态扫描看不到,
于是要手动点名。这是个典型坑:不写这个,打包出的 exe 一运行就报
ModuleNotFoundError。

打包结果:

代价:exe 通常很大(几十 MB),因为里面塞了一整个 Python 解释器和所有依赖。
这也是为什么很多项目选择"让用户自己装环境跑"而不是发 exe。

块 5:文档 —— 别人怎么知道它是干嘛的

看 worry_debate_game/README.txt 的结构:

==============================
  天使与恶魔 — 烦恼辩论
  启动说明
==============================
【方式一】双击 start.bat(推荐)
【方式二】命令行启动
【方式三】打包的 exe
【注意事项】……
【API Key 获取】……

一份合格的 README 应该回答四个问题:

  1. 这是什么?(一句话)
  2. 怎么装/怎么跑?(越快越好,最好双击)
  3. 怎么用?(快捷键、设置在哪)
  4. 出问题怎么办?(常见错误、依赖缺失)

项目里的 README 已经覆盖了这些。小改进:加一句"技术栈"和"截图",
别人(和 HR)会更愿意点进来。

块 6:版本管理 —— 项目的时间机器

这里浓缩几条纪律:

git status                          REM 先看有什么改动(别盲提)
git add .                           REM 暂存
git commit -m "修复:xxx"            REM 提交,说明写"为什么"
git log --oneline                   REM 回顾历史

几条纪律:

块 7:发布 —— 让别人能用上

把项目发到 GitHub 的最小步骤(在项目目录里执行):

git remote add origin https://github.com/你的用户名/仓库名.git
git push -u origin main

发布前自检清单:

进阶层:在 GitHub 上发 Release,把打包好的 exe 作为附件传上去——
别人不用装 Python,下载即用。这是"作品"和"玩具"的区别之一。

再进阶层(以后再说):GitHub Actions 自动测试——
每次推送代码,服务器自动跑 pytest,挂了就邮件/界面提醒。这就是持续集成(CI)。


第四步:对照开源,成熟项目还多什么

去 GitHub 看一个认真维护的项目,根目录通常会看到:

文件作用教材有吗
README.md说明书✅ 大部分项目有
LICENSE授权协议待补
.gitignore排除不该提交的东西✅ 已全部配好
requirements.txt / pyproject.toml依赖✅ 部分有,建议补齐
tests/测试✅ rm_v2、resources-keeper 有
CONTRIBUTING.md怎么参与贡献进阶
CHANGELOG.md版本变更记录进阶
.github/workflows/自动测试(CI)进阶

你不需要一次全有。 先把 README、依赖、.gitignore 三样做扎实,
项目就已经超过大多数"课设级"作品了。


动手练习(做完才算过关)

练习 1(必做):给「查资料」写第一个测试
它目前没有测试。新建 查资料/tests/test_basic.py:

import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

def test_make_summary():
    import builtins
    # 先把 server.py 里的函数拿出来用(提示:可以 import server)
    ...

提示:server.py 里有纯函数 make_summary、make_title、now_str,
很适合拿来练手(它们不依赖数据库、不依赖网络)。
写 3 条测试,然后用 py -3 -m pytest 查资料/tests -v 跑通。

练习 2(必做):体验"测试救了你"
把 test_models.py 里 test_category_creation 改成:

assert cat.name == "WrongName"

跑 pytest,看它如何精确告诉你哪个测试挂了、期望什么、实际什么。然后改回来。
记住这种感觉:以后重构完,先跑测试。

练习 3(必做):给一个项目补依赖清单
给「鬼谷 Mod 管理器」补 requirements.txt(一行:PyQt6),
给「词源机器人」检查 requirements.txt 是否完整(对比它 import 的库)。
"别人 clone 下来能不能跑起来",就靠这一步。

练习 4(选做):打一次包并解释参数
运行 worry_debate_game/build.bat,成功后:
(1)看 dist/ 里 exe 的体积,说说为什么这么大
(2)找到 build/ 或生成的 .spec,说说它们的作用
(3)把 --hidden-import uvicorn.logging 删掉重新打包,运行 exe,
看看会发生什么错误——你就理解了这个参数的必要性(实验完记得改回来)。

练习 5(选做,发布):把一个项目推上 GitHub
完成自己的项目后,选一个你最有信心的项目:
(1)在 GitHub 建空仓库 (2)按上面"块 7"的命令推送
(3)(进阶)发一个 Release,把 exe 或压缩包附上。

练习 6(收尾):把工程化七件套的勾打齐
对照"第二步"的七件套,做一个"正式化"的项目,
补完它就从"练习"升级成了"作品"。


本课小结

术语表

术语人话解释
工程化让项目可维护、可交付、可信任的一整套做法
单元测试 / 集成测试测单个零件 / 测多个零件配合
fixturepytest 的测试环境准备与清理机制
断言(assert)"我认为结果应该是这样"的检查
覆盖率测试执行到的代码比例
隔离测试用临时数据,不污染真实数据
PyInstaller把 Python 程序打包成可执行文件的工具
--hidden-import手动指定动态导入、扫描不到的模块
ReleaseGitHub 上的正式发布版本(可附安装包)
CI(持续集成)每次提交自动跑测试的机制
LICENSE授权协议,说明别人能怎么用你的代码

课程回顾与下一步

恭喜!12 课全部走完。你现在应该能:

现在,你已经比自己想象中走得更远了。