Screenshot to Code 后端测试实战:pytest 运行、配置解析与测试编写规范
本文基于 screenshot-to-code 仓库的 TESTING.md 测试指南展开,完整覆盖后端测试的运行命令(全量、定向、覆盖率、并行)、pytest.ini 中每一项配置的实际含义,并结合 backend/pyproject.toml 的依赖声明与 backend/tests 目录下 40 余个真实测试文件,讲解如何按仓库既有风格为 FastAPI 后端编写新的 pytest 测试。
测试体系概览
Screenshot to Code 的后端是一个 FastAPI 服务(backend/main.py),其测试体系基于 pytest + Poetry 构建:
- 所有测试统一放在
backend/tests/目录(即 backend/tests); - 测试规模:该目录下共有 40 个测试文件、约 7200 行测试代码,覆盖路由(test_screenshot.py)、Agent 引擎与工具运行时(test_agent_engine.py、test_agent_tool_runtime.py)、多模型 Provider 会话(test_openai_provider_session.py、test_anthropic_many_image_limit.py、test_gemini_provider_session.py)、Prompt 构造(test_prompts.py)、Token 用量与成本(test_token_usage.py)、Evals 评测系统(test_eval_runner.py、test_eval_sessions.py)等核心模块;
- 异步测试依赖
pytest-asyncio,WebSocket 等 FastAPI/Starlette 异步组件的测试在仓库中已有成熟范式(见下文示例)。
说明:官方 TESTING.md 的测试指南聚焦后端。前端(
frontend/)另有基于 Jest 的独立测试体系(frontend/jest.config.js),不在本文范围内。
前置条件:环境准备
测试运行依赖 Poetry 环境。按文档要求,先安装全部依赖:
cd backend
poetry install
从 backend/pyproject.toml 可以确认几个关键前提:
| 配置项 | 取值 | 说明 |
|---|---|---|
| Python 版本 | ^3.10 |
需要 Python 3.10 及以上 |
| 测试框架 | pytest = "^7.4.3" |
位于 dev 依赖组 |
| 异步支持 | pytest-asyncio = "^0.21" |
位于 dev 依赖组,配合 asyncio_mode = auto |
| 类型检查 | pyright = "^1.1.352" |
用于类型检查,与测试运行相互独立 |
测试框架与异步插件都声明在 [tool.poetry.group.dev.dependencies] 中,因此 poetry install(默认安装含 dev 组)后即可直接运行测试。此外注意:pytest-xdist 没有被写进 pyproject.toml,如需并行测试要按文档另行安装(见下文)。
运行测试:从全量到定向的完整命令集
运行全部测试
cd backend
poetry run pytest
由于 backend/pytest.ini 中配置了 testpaths = tests,pytest 会自动将收集范围限定在 tests/ 目录,无需在命令行重复指定路径。
更详细的输出
poetry run pytest -vv
-vv 在默认 -v 的基础上再提升一级详细度,适合在调试单个模块时观察用例间的依赖与参数展开。
定向运行:文件 / 类 / 方法三级粒度
pytest 的 :: 选择器支持逐级缩小运行范围,这是日常开发中最常用的三种姿势:
# 只运行某个测试文件
poetry run pytest tests/test_screenshot.py
# 只运行某个测试类
poetry run pytest tests/test_screenshot.py::TestNormalizeUrl
# 只运行某个测试方法
poetry run pytest tests/test_screenshot.py::TestNormalizeUrl::test_url_without_protocol
以 test_screenshot.py 为例,TestNormalizeUrl 类验证的是 backend/routes/screenshot.py 中 normalize_url() 的行为:无协议时补全 https://、保留已有的 http/https、对 ftp://、file:// 等不支持的协议抛出 ValueError。定位到某一失败用例后,用方法级选择器单独重跑是最快的验证方式。
带覆盖率报告运行
poetry run pytest --cov=routes
--cov 参数指定要统计覆盖率的包。仓库路由层位于 backend/routes/,上例即可输出该目录的覆盖率报告;换成 --cov=agent 或 --cov=prompts 可以分别评估 Agent 引擎和 Prompt 构造层的覆盖情况。注意:pytest-cov 同样属于按需安装的插件,若当前环境未安装需先通过 Poetry 添加。
并行运行(需要 pytest-xdist)
poetry install --with dev pytest-xdist # Install if not already installed
poetry run pytest -n auto
-n auto 会按 CPU 核心数自动分配 worker。这里要特别留意:pytest-xdist 未包含在 backend/pyproject.toml 的依赖声明中,属于测试时的可选增强。若你使用的 Poetry 版本不识别上述安装语法,等价的标准写法是先以 poetry add --group dev pytest-xdist 将插件加入 dev 依赖组。
测试配置解析:backend/pytest.ini 逐行说明
测试行为由 backend/pytest.ini 统一控制,完整内容如下:
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
asyncio_mode = auto
TESTING.md 中列出了前五项的约定,这里逐项补充其实际效果,并补上文档未展开的最后一项:
| 配置项 | 值 | 作用 |
|---|---|---|
testpaths |
tests |
裸跑 pytest 时自动限定收集目录,即 backend/tests/ |
python_files |
test_*.py |
只收集 test_ 前缀的文件为测试模块 |
python_classes |
Test* |
只收集 Test 前缀的类为测试类(不带下划线) |
python_functions |
test_* |
只收集 test_ 前缀的函数为测试用例 |
addopts |
-v --tb=short |
默认开启详细输出与短 traceback,无需手动加 -v |
asyncio_mode |
auto |
pytest-asyncio 自动模式:异步测试函数无需显式装饰即可被正确驱动 |
关于 asyncio_mode = auto:它来自 pytest-asyncio 插件,开启后所有 async def test_* 函数都会被自动按异步协程处理。值得注意的是,仓库中的实际写法(如 test_websocket_communicator.py)仍然为每个异步用例显式标注了 @pytest.mark.asyncio——两种写法在 auto 模式下都有效,显式标注让意图更清晰,也是本仓库的既有风格。
编写新测试:仓库约定与真实范例
TESTING.md 给出的三步流程依然成立:
- 在
backend/tests/下按test_<module>.py命名约定新建文件; - 导入待测的函数 / 类;
- 按 pytest 约定编写测试函数或测试类。
文档中的最小示例:
import pytest
from routes.screenshot import normalize_url
def test_url_normalization():
assert normalize_url("example.com") == "https://example.com"
这个示例与仓库真实代码完全对得上:normalize_url 定义于 backend/routes/screenshot.py,对应的完整测试类 TestNormalizeUrl 涵盖了无协议补全、协议保留、路径与查询参数、空白裁剪、非法协议报错、localhost 与 IP 地址等 8 组场景。
风格建议:用类组织相关用例
纯函数测试可以用顶层 test_* 函数,但仓库中多数文件采用 Test 类 + 语义化方法名 的组织方式,例如 test_screenshot.py:
class TestNormalizeUrl:
"""Test cases for URL normalization functionality."""
def test_url_without_protocol(self):
"""Test that URLs without protocol get https:// added."""
assert normalize_url("example.com") == "https://example.com"
assert normalize_url("www.example.com") == "https://www.example.com"
类 docstring 说明被测功能,方法 docstring 说明具体断言语义,这使 pytest -vv 的输出和失败报告都具备自解释性。
异步组件的测试范式:MagicMock + AsyncMock
后端大量组件是异步的(WebSocket 通信、Provider 会话等),test_websocket_communicator.py 展示了仓库处理这类测试的标准做法——用 MagicMock/AsyncMock 替换 FastAPI/Starlette 的 WebSocket,避免真实网络依赖:
from unittest.mock import AsyncMock, MagicMock
from starlette.websockets import WebSocketDisconnect
from routes.generate_code import WebSocketCommunicator
@pytest.mark.asyncio
async def test_receive_params_marks_websocket_closed_on_disconnect() -> None:
websocket = MagicMock()
websocket.receive_json = AsyncMock(side_effect=WebSocketDisconnect(1006))
communicator = WebSocketCommunicator(cast(Any, websocket))
with pytest.raises(WebSocketDisconnect):
await communicator.receive_params()
assert communicator.is_closed is True
该文件还专门模拟了 "ASGI message 'websocket.close' after response already completed" 这类真实生产中的竞态异常,验证 WebSocketCommunicator 在连接已完成后不会二次抛错(backend/tests/test_websocket_communicator.py)。为异步测试新写用例时,直接参照这一范式即可。
用异常断言覆盖错误路径
pytest.raises 是仓库中验证错误路径的惯用手段,例如 test_screenshot.py 对非法协议的双重断言(同时匹配异常类型和错误消息):
def test_invalid_protocols(self):
"""Test that unsupported protocols raise ValueError."""
with pytest.raises(ValueError, match="Unsupported protocol: ftp"):
normalize_url("ftp://example.com")
结语:从源码结构看测试与模块的对应关系
TESTING.md 给出的命令集配合 backend/pytest.ini 的收集约定,构成了一个可直接上手的测试工作流;而 backend/tests/ 目录的组织方式(test_agent_*、test_openai_*、test_evals_* 等与 agent/、routes/、evals/ 目录名一一对应)也表明:仓库倾向于按被测模块命名测试文件,新模块的测试可以沿用同样的映射规则。日常开发时建议的闭环是:pytest tests/<文件>::<类>::<方法> 快速复现单个失败 → 修复后用 --cov=<模块> 确认覆盖未回退 → 最后全量 poetry run pytest 保证无回归。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00