测试、调试与打包:从 pytest 到 PyInstaller exe
复盘 RespiraScope 如何通过单元测试覆盖滤波、峰谷、Socket.IO、记录状态和 HTTP 默认值,并解释源码运行、onefile exe、onedir 包的取舍。
背景问题
实时系统最怕“看起来能跑”。只要传感器有数据,页面有曲线,很多问题会被掩盖:队列满了是否丢数据,Socket 新连接是否补快照,Record 前置条件是否正确报错,短数据是否会误滤波,端口占用是否会自动避让。
RespiraScope 的测试和打包目标不是追求覆盖率数字,而是覆盖这些最容易破坏用户体验的工程边界。
测试方案对比
| 方案 | 优点 | 问题 | 项目判断 |
|---|---|---|---|
| 只手动打开页面看波形 | 最接近使用体验 | 不可重复,难覆盖错误状态 | 不足够 |
| 单元测试算法函数 | 快速稳定 | 不能覆盖 HTTP 和状态机 | 必须有但不够 |
| FastAPI TestClient | 不启动真实服务也能测接口 | 不覆盖浏览器行为 | 当前采用 |
| 端到端浏览器测试 | 最完整 | 成本高,依赖运行环境 | 可作为后续增强 |
当前测试重点放在后端核心行为:滤波、峰谷、队列、Socket.IO、HTTP 默认参数、记录状态、异常质量事件和端口选择。
当前测试覆盖
| 测试文件 | 覆盖重点 |
|---|---|
test_signal_processing.py | 实时/离线滤波、短数据保护 |
test_peak_valley_detector.py | 峰谷识别、BPM 和稳定性 |
test_runtime_resilience.py | 队列 dropped、高水位、快照、质量事件、record 状态 |
test_http_defaults.py | 空请求体默认值、/applyFilter 空数据 |
test_socket_io_service.py | 多客户端广播和新连接快照 |
test_port_selection.py | 端口可用性和自动选择 |
这里最值得学习的是测试对象选择。项目没有试图在单元测试里启动完整浏览器,而是把可重复的核心逻辑拆出来测。比如 DataQueueManager.snapshot_messages() 可以直接测试,不必通过浏览器刷新来观察。
调试路径
当系统没有数据时,不应该先怀疑图表。推荐按这条路径排查:
GET /health:后端是否运行,mock 是否启用,传感器地址是什么。GET /runtime/config:前端看到的 backend port 是否和真实端口一致。GET /stream/status:是否 started,socket 是否 connected,received_count 是否增长。- Socket.IO:是否收到
raw类型消息。 - Filter:raw 有数据但 filtered 没数据时,检查是否少于
FILTER_STARTUP_DELAY = 300。 - Record:记录按钮不可用时,检查是否已经开始接收并收到 raw 点。
打包方案对比
| 方案 | 优点 | 问题 | 适合场景 |
|---|---|---|---|
源码运行 uv run RespiraScope | 方便开发,依赖可复现 | 用户需要安装 Python 和 uv | 开发调试 |
| PyInstaller onefile | 分发简单,单个 exe | 首次启动解包慢,调试不方便 | 演示和交付 |
| PyInstaller onedir | 文件多但结构清楚 | 分发不如单文件方便 | 打包问题排查 |
| Docker | 环境一致 | Windows 设备联调和本地传感器访问不一定方便 | 服务部署 |
项目默认使用 PyInstaller onefile 输出 dist/RespiraScope.exe。如果需要排查静态资源或隐藏导入问题,可以用 --onedir 生成展开目录包。
为什么打包脚本要显式收集依赖和前端资源
SciPy、NumPy、uvicorn、socketio、engineio 都有动态导入或二进制组件。PyInstaller 不一定能完全自动发现,所以脚本里使用了:
--collect-all numpy
--collect-all scipy
--collect-submodules socketio
--collect-submodules engineio
--collect-submodules uvicorn
前端静态资源也必须通过 --add-data 嵌入:frontend-console、frontend-monitor、frontend-lab、frontend-guide、frontend-api-docs 和配置模板都要进入 exe。否则后端启动了,Console 可能找不到静态文件。
代价和边界
单元测试不能证明真实设备一定稳定,只能证明核心逻辑在可控输入下行为正确。真实传感器还涉及网络、设备固件、线缆、采样间隔和现场干扰。
PyInstaller 也不是部署银弹。onefile exe 方便分发,但启动时会解包,杀毒软件也可能影响启动速度。生产环境如果需要持续运行、日志采集和自动重启,仍应考虑 Windows 服务、进程守护或容器化部署。
关键函数表
| 函数或工具 | 作用 | 设计点 |
|---|---|---|
pytest | 运行单元测试 | 快速验证核心行为 |
TestClient | 测 FastAPI 路由 | 不启动真实网络服务也能测接口 |
asyncio.run() | 测异步滤波和 Socket 行为 | 让异步逻辑可重复测试 |
uv sync | 同步依赖 | 锁定 Python 依赖 |
uv run RespiraScope | 源码启动 | 开发和调试主入口 |
PyInstaller | 构建 exe | 分发给非 Python 用户 |
--add-data | 打包静态资源 | 确保 Console 和配置模板可用 |
读源码建议
先读 tests/test_runtime_resilience.py,它覆盖了队列、快照、质量事件和 record 状态,是理解系统边界最好的测试文件。再读 scripts/build_exe.py,看为什么打包脚本要显式收集依赖和静态资源。最后执行 uv run pytest,把测试输出和源码行为对应起来。