测试与规范
测试框架
| 工具 | 版本 | 用途 |
|---|---|---|
| pytest | ~9.0 | 测试运行器 |
| pytest-qt | ~4.5 | Qt UI 测试 |
| pytest-cov | ~5.0 | 覆盖率统计 |
| pytest-asyncio | ~1.3 | 异步测试支持 |
运行测试
统一走 scripts/run_tests.sh(默认 validate)。它自己解析 .venv 里的解释器, 不依赖你先把虚拟环境激活:
scripts/run_tests.sh # validate:全部业务/DB/计算,跳过 Qt
scripts/run_tests.sh fast # 纯计算/轻服务白名单
scripts/run_tests.sh ui-retest # 只有 Qt 界面 + 真 QThread,改动涉及 UI 时优先
scripts/run_tests.sh target # 只跑 git 变更相关;纯文档/配置变更会自动跳过
scripts/run_tests.sh full # validate + ui-retest 两阶段耗时(2026-09-17 实测,空载):
validate58s、ui-retest221s、full≈ 4.7min。 机器负载高时ui-retest曾达 445s(full≈ 8.4min)—— 差别很大,别按某一个数去卡。 变化趋势见CLAUDE.md的「测试边界」。target的耗时取决于变更范围(改一个服务可能只选中几个文件,改跨层模块会选中上百个)—— 没有固定值,纯文档/配置变更则秒退。
开发循环用 fast / target,日常回归用 validate;full 的时机由用户定,不主动跑。
想直接调 pytest 也可以(必须用虚拟环境里的解释器,裸 python 可能是没装 pytest 的 系统解释器,失败会被管道吞掉):
.venv/Scripts/python.exe -m pytest tests/ -q -m "not ui"
.venv/Scripts/python.exe -m pytest tests/test_scoring_service.py -v
.venv/Scripts/python.exe -m pytest tests/ -k "test_calc" -v
.venv/Scripts/python.exe -m pytest --lf # 只重跑上次失败的⚠️ 本地跑 pytest 时不要自己设
QT_QPA_PLATFORM。测试套件没有设它:Windows 上默认走原生平台,offscreen只给 Linux CI(runner 无显示器)与scripts/shell_snapshot.py用。 在 Windows 上强行QT_QPA_PLATFORM=offscreen跑test_qml_*.py会让PageHost加载页签时挂死 —— 表现为卡满pyproject.toml里faulthandler_timeout = 120的整点超时、栈停在tests/qml_page_load.py::page_host(2026-09-26 实测,同一用例去掉该变量后 1.3s 通过)。这条现在有硬门:
tests/conftest.py的offscreen_is_forbidden()在 「Windows +QT_QPA_PLATFORM=offscreen」时会直接拒绝这次跑测并打印原因(逃生开关EVE_ALLOW_OFFSCREEN_TESTS=1)。2026-10-05 又踩了一次同一个坑:tests/test_qt_noise.py真平台 2.36s 通过、offscreen 卡死;tests/test_qml_dialogs.py同样是「offscreen 卡第一个用例、 真平台全绿」。判定表见tests/test_test_env_guards.py。
测试分档与标记
档位由 pyproject.toml 的 markers 驱动,只有两个:
| 标记 | 含义 | 归入哪一档 |
|---|---|---|
fast | 纯计算 / 轻服务白名单 | fast 档 |
ui | Qt 界面 + 真 QThread | ui-retest 档 |
| (无标记) | 其余全部 | validate 档 |
pytestmark = pytest.mark.ui # 整个文件是 Qt 用例时的常见写法validate(-m "not ui")与 ui-retest(-m ui)互斥,二者并集恰好覆盖全部用例、 恒等于 full —— 一个用例必须落进其中一档,没有「两档都不跑」的漏网空间。
测试文件组织
tests/ 下 130+ 个文件,按被测对象命名。几个大类:
| 前缀 | 覆盖 |
|---|---|
test_qml_* | QML 页面 / 对话框 / 桥 / 模型(占最大一块) |
test_plan_* | 生产计划、拆解、执行 |
test_workers_* | 后台 QThread |
test_industry_* | 工业页控制器与对话框 |
test_theme_* | 主题注册表与监听器 |
test_*_models | 表格模型 |
| 其余 | 各自的 service / 领域模块 |
不要在此处维护完整清单 —— 它必然过时。权威来源是 tests/ 目录本身。 (README 里原先有硬编码的测试总数、由 scripts/check_readme.py + 提交钩子强制同步,该机制已移除: 那个数字每增删一个用例就要改一次,只会制造无意义的提交摩擦。)
⚠️ 与 QML 相关的本仓特有约定(写在各自的 test_qml_*.py 里)。 新增用例前先读 CLAUDE.md 的「测试边界」、克制条款第 4 条与「测试判定表」 —— 测试有硬预算,不按覆盖率补用例。先过判定表确认这次改动到底要不要写测试:外观断言 / 标准库断言 / 仅非 None / mock 调用次数都在禁止档; 判定表外的用例可用 python scripts/test_audit.py 自查。
- 新对话框并入统一的「加载无 QML 告警」冒烟用例(
tests/test_qml_dialogs.py的_assert_loads_and_quiet)—— 缺 import / 绑错属性这类问题只在运行时吐一条,静态扫描看不见。 不要再为每个新对话框新建一个独立用例:那只是同一个断言重复 N 遍,往现有参数化列表里加一行即可; - 「只有真窗口才看得见」的东西不写测试,用截图核对:字形(离屏下
QFontDatabase是 0 个字体, 文字全成方框)、DWM 毛玻璃、窗口拖动/缩放 —— 跑scripts/shell_snapshot.py --real看; - 布局/几何/尺寸/像素一律不写断言(布局一改就红,与业务无关)。确需交互用例时, 点击坐标由
tests/qml_click.py统一算,不要在用例里手写像素值。
测试 Fixtures
conftest.py 提供的关键 fixtures:qapp / app(含 FluentWinUI3 设置)、 temp_db / db_manager(四库临时数据库)、mock_db、main_window(QML 外壳)、 industry_page(工业页控制器)。
几条 autouse 的全局安全网,加测试时值得知道它们的存在:
no_auto_price_download—— 阻断应用自动发起的网络(价格检查/下载、SDE/ESI 初始化、 工业数据拉取 worker)。 它挂在「会自己发请求的那几个入口」上,不挂在某一层外壳的私有方法上(那样外壳一换就整片 setup 炸);isolate_user_settings—— 把settings.json指向临时文件,测试绝不写用户真实数据;_reset_qt_noise_state—— 复位core.qt_noise的进程级退出标记(不复位会让后续用例的notify()静默变空操作)。
代码质量工具
ruff — Lint + Format
ruff check . # 检查
ruff check . --fix # 自动修复
ruff format . # 格式化
ruff format --check . # 检查格式配置在 pyproject.toml:line-length = 120、target-version = "py313", 启用 E/W/F/B/I/M/C4/UP(忽略 E501,交给格式化器)。
mypy — 类型检查
mypy .配置要点:python_version = "3.14"、warn_unused_configs / warn_redundant_casts 开启, 第三方库走 ignore_missing_imports。
提交前检查清单
ruff check . && mypy . && scripts/run_tests.sh targetfull 的时机由用户定 —— 它是 5~8 分钟的全量档,不是每次提交的例行公事。
pre-commit 钩子(.pre-commit-config.yaml,git commit 时自动跑):ruff / ruff-format / mypy、 行尾空白、文件末尾换行、YAML 校验、大文件检查、文档站内部链接(scripts/check_docs_links.py)。 (原先还有 gen-api-docs / check-readme / docs-stale-reminder 三个文档联动钩子,因制造提交摩擦已移除; 需要时手动跑 python scripts/gen_api_docs.py。)
CI 检查
.github/workflows/ci.yml,在 push main/develop 与 PR 到 main 时运行两个 job:
test(Python 3.14 + uv sync --dev,Linux runner):
- ruff —
ruff check .(不跑ruff format --check .:仓库存在历史未格式化文件,跑则 CI 恒红,整改应单独立项) - mypy —
mypy . --ignore-missing-imports - pytest — 全量 + 覆盖率,
QT_QPA_PLATFORM=offscreen(无头 Qt;runner 需装libegl1/libgl1) - 文档站内部链接校验 —
scripts/check_docs_links.py - Codecov — 上传
coverage.xml
version-check(独立 job):scripts/check_version.py 校验 core/version.py == CHANGELOG 最新版本段 == pyproject.toml [project].version == git tag(发版态), 需 fetch-depth: 0。