十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Streamlit Python 单元测试指南:从测试编写规范到运行与类型检查实战

Streamlit Python 单元测试指南:从测试编写规范到运行与类型检查实战 Streamlit Python 单元测试指南从测试编写规范到运行与类型检查实战【免费下载链接】streamlitStreamlit — A faster way to build and share data apps.项目地址: https://gitcode.com/gh_mirrors/st/streamlit本文以 Streamlit 仓库中的官方测试文档 lib/tests/AGENTS.md 为核心骨架系统讲解 Streamlit Python 代码库的单元测试编写规范、运行方式与静态类型测试机制。读者将掌握如何编写符合 Streamlit 代码库规范的高质量 pytest 测试函数式风格、参数化、防回归断言、集成依赖标记如何通过make与uv快速运行全部或单个测试以及如何理解并扩展lib/tests/streamlit/typing/下的公共 API 类型测试。Streamlit 的单元测试unit tests用于覆盖那些无需 web 前端与后端配合即可验证的内部行为是保障 Python 核心逻辑稳定性的第一道防线。仓库明确设定了95% 的 Python 单元测试覆盖率目标针对lib/streamlit这意味着理解本文的规范不仅是贡献代码的前提也是维护代码库健康度的基本要求。一、单元测试的定位与覆盖率目标在 Streamlit 仓库中测试被清晰地分层单元测试unit tests位于lib/tests/只依赖 Python 侧代码不需要启动浏览器或 web 服务。本文档聚焦于此。E2E 测试Playwright位于 e2e_playwright/需要前后端共同运行覆盖真实交互场景。静态类型测试typing tests位于 lib/tests/streamlit/typing/不是 pytest 测试而是由类型检查器mypy / ty直接执行。单元测试的目标是覆盖可以在没有 web / 后端对应物的情况下工作的内部行为例如配置解析、会话状态管理、缓存逻辑、Delta 生成器等纯 Python 逻辑。正因为如此单元测试可以做到快速、稳定、可并行成为 CI 中最高频的质量关卡。二、测试编写核心原则2.1 优先函数式 pytest而非 unittest.TestCase新测试文件默认使用独立的def test_*函数只有当你确实需要unittest.TestCase的特性例如setUp/tearDown生命周期、特定的断言辅助方法时才使用它。仓库中大量测试遵循这一原则例如 lib/tests/streamlit/auth_util_test.py 与 lib/tests/streamlit/command_suggestions_test.py 均以独立函数配合pytest.mark.parametrize编写。函数式风格的优势在于每个测试独立、扁平失败时定位更直接天然支持 pytest 的 fixture 注入、参数化与 marker无需继承带来的样板代码。2.2 docstring 与类型注解每个新测试函数都应添加一段简短的numpydoc 风格docstring说明该测试验证的行为。新测试应完整标注类型。仓库的 ruff 配置对lib/tests/**放宽了部分规则见 pyproject.toml 中的[tool.ruff.lint.per-file-ignores]忽略ANN、D、DOC等规则允许测试函数不强制完整类型注解与 docstring但文档仍明确要求新测试遵循上述规范以保证测试代码的可读性与可维护性。2.3 导入位置默认顶层导入导入应位于测试文件的顶层。仅在确有特殊原因时才在测试函数内部导入例如集成依赖需要延迟导入见下文require_integration存在循环导入问题测试的目标本身是导入行为在AppTest函数内需要特殊处理。2.4 集成依赖与pytest.mark.require_integration这是本文档最关键的实战约束之一。根目录 pyproject.toml 的[dependency-groups]中定义了integration依赖组包含pydantic、sympy、polars、sqlalchemy以及xarray、dask、duckdb、scipy、streamlit[snowflake]等重量级依赖。这些包仅安装在集成测试环境中常规单元测试环境不包含它们。因此使用这些包的测试必须同时满足两个条件在测试函数内部导入而不是模块顶层——否则在无集成环境的 CI 上会直接ImportError添加pytest.mark.require_integrationmarker——这样在非集成环境下运行时会优雅跳过。一个真实的仓库示例是 lib/tests/streamlit/external/pydantic_integration.py它在函数内部执行from pydantic import ...并标注pytest.mark.require_integrationpytest.mark.require_integration class PydanticIntegrationTest(unittest.TestCase): def pydantic_model_definition(self): from pydantic import ( # type: ignore[import-not-found] BaseModel, root_validator, validator, ) ...marker 的跳过逻辑由 lib/tests/conftest.py 中的pytest_runtest_setup实现当 pytest 收到--require-integration参数时只运行带该 marker 的测试反之不带该参数时则跳过带 marker 的测试。此外pytest_configure会在启用--require-integration时校验snowflake.snowpark是否已安装未安装直接报pytest.UsageError。2.5 依赖 CI 密钥的测试需要 CI secrets 的测试若对应环境变量未设置应通过pytest.mark.skipif跳过避免本地或非 CI 环境下误报失败。2.6 参数化测试新测试使用pytest.mark.parametrize合并仅输入 / 期望输出不同的测试用例保持测试套件简洁、易维护。遗留的unittest.TestCase类测试使用parameterized.expand来自parameterized包已列入 pyproject.toml 的 test 依赖组。2.7 防回归断言anti-regression assertions测试不应只覆盖 happy path还应在实际可行的范围内覆盖一个合理的失败模式或边界情况例如测试非法输入抛出预期异常测试边界条件空列表、0、None、最大长度断言某个副作用没有发生如只读操作不得修改状态断言返回值不包含某个看似合理但错误的条目如过滤函数不得包含被排除项。同时文档给出了一条重要的减负原则不要添加逻辑上已被同测试中先前断言蕴含的断言。例如已经断言x is True就不要再断言x is not False——这是同义反复没有价值。同样不要同时断言简单布尔值或枚举的两侧。2.8 针对性否定优于穷举矩阵优先为每个行为添加一个高信号的否定检查不要在没有回归历史的情况下盲目膨胀测试用例数量。换句话说宁可少而精不要为了数量凑矩阵。三、如何运行单元测试3.1 运行全部单元测试在仓库根目录执行make python-tests该命令由 Makefile 中的python-tests目标定义实际展开为MPLBACKENDAgg uv run pytest -c lib/pyproject.toml -v -l \ -m not performance \ lib/tests/几个关键细节MPLBACKENDAgg避免 matplotlib 在 macOS 上因默认macosx后端必须在主线程运行而导致解释器崩溃-c lib/pyproject.toml使用 lib/pyproject.toml 中的 pytest 配置testpaths [tests]、markers 声明、filterwarnings以及addopts --covstreamlit --cov-reporthtml --cov-configlib/pyproject.toml即每次运行都会生成覆盖率报告-m not performance排除性能基准测试性能测试另有make python-performance-tests使用--benchmark-autosave与--benchmark-storage。3.2 运行单个测试文件uv run pytest lib/tests/streamlit/my_example_test.py3.3 运行文件中的某个测试使用-k关键字表达式按名称筛选uv run pytest lib/tests/streamlit/my_example_test.py -k test_that_something_works-k支持子串、and/or/not等表达式可用于一次筛选多个目标测试。3.4 运行集成测试需要先在根目录安装集成依赖组PYTHON_DEPENDENCY_GROUPintegration make python-init然后运行make python-integration-tests该目标使用uv run --no-sync pytest ... --require-integration lib/tests/。--no-sync是关键普通uv run会重新同步到默认的dev组并卸载集成专属依赖而--no-sync保留刚安装的 integration 环境。--require-integration选项与 marker 逻辑均由 lib/tests/conftest.py 的pytest_addoption/pytest_runtest_setup提供。四、静态类型测试Typing Testslib/tests/streamlit/typing/目录下的文件如button_types.py、dataframe_types.py、write_types.py等覆盖几乎所有公共 API不是 pytest 测试。它们是对公共 API 的静态类型检查测试当类型推断逻辑涉及 TypeVar、overload 等较复杂机制时仅靠行为测试无法覆盖静态类型结果是否正确因此需要显式断言某些类型推断结果确实成立。运行方式make python-types该目标见 Makefile依次执行uv run ty check # ty 类型检查器 uv run mypy # mypy配置读取自根 pyproject.toml并会对lib/streamlit/.agents/下的模板应用逐文件运行 mypy因为这些模板共用模块名streamlit_app且位于点前缀目录下批量检查会触发duplicate module错误。仓库的 mypy 配置根 pyproject.toml对测试目录做了精细区分module tests.*ignore_errors true单元测试默认不强制类型检查module tests.streamlit.typing.*ignore_errors false显式开启类型测试的类型检查ty 的[tool.ty.src]中同样将lib/tests/streamlit/typing列入include。4.1 如何书写类型断言按照 lib/tests/streamlit/typing/README.md 的说明故意非法的调用使用# type: ignore[...]mypy与# ty: ignore[...]ty标注一个合法的调用若其断言类型 mypy 接受但 ty 拒绝可使用# ty: ignore[type-assertion-failure]并添加简短注释说明 ty 推断出的类型以便 ty 追赶上来之后移除该抑制。五、测试基础设施与配置速查5.1 全局 fixtures 与 conftestlib/tests/conftest.py 提供了全仓库测试的基础设施在导入任何测试模块前用 mock 的配置文件内容重新解析 Streamlit 配置[global] unitTest true、[browser] gatherUsageStats false并强制config.get_config_options(force_reparseTrue)以捕获首次导入即调用config.get_option()的违规行为anyio_backendfixture将pytest.mark.anyio测试固定到 asyncio 后端Streamlit 只运行在 asyncio 上benchmarkfixture仅允许带pytest.mark.performance的测试使用并在pytest_collection_modifyitems中自动把使用benchmarkfixture 的测试标记为performanceenable_mpa_v2_mode辅助函数为需要 MPA v2 模式前提的测试设置 PagesManager 状态。5.2 测试相关依赖组根 pyproject.toml依赖组主要内容用途dev包含test组、pre-commit、ty0.0.80、ruff0.16.7、mypy2.3.1、pandas-stubs3.0.0 等默认开发环境uv sync默认安装testpytest9.1.0、pytest-cov、hypothesis、parameterized、requests-mock、testfixtures、pytest-xdist、pytest-rerunfailures排除 16.0、pytest-benchmark、pytest-timeout、playwright1.62.0、seaborn、watchdog、rich、vega_datasets 等单元测试与 E2E 测试依赖integration包含test组、streamlit[snowflake]、polars、xarray、dask、duckdb、sqlalchemy[mypy]、scipy、pydantic、sympy集成测试专用依赖需--require-integration配合仓库明确说明uv.lock是安装版本的事实来源依赖组只声明包名与必要约束这保证了不同 CI 环境下测试依赖的一致性。六、写在最后贡献测试时的自查清单结合本文档与仓库配置向 Streamlit 提交 Python 代码或测试时可对照以下清单新测试是否使用了函数式def test_*除非确实需要unittest.TestCase特性每个新测试函数是否有 numpydoc 风格 docstring、完整类型注解导入是否在顶层若涉及pydantic/sympy/polars/sqlalchemy等集成依赖是否改在函数内部导入并添加pytest.mark.require_integration是否用pytest.mark.parametrize合并了重复用例避免穷举矩阵是否在 happy path 之外补充了高信号的边界 / 失败用例同时避免同义反复断言运行验证make python-tests全量、uv run pytest file -k name定向调试、涉及集成依赖时用make python-integration-tests若改动涉及公共 API 的类型推断是否同步在 lib/tests/streamlit/typing/ 下补充类型断言并用make python-types验证遵循这套规范既能保证 Streamlit 维持 95% 的单元测试覆盖率目标也能让每个测试在快速、稳定的前提下具备真正的回归防护价值。【免费下载链接】streamlitStreamlit — A faster way to build and share data apps.项目地址: https://gitcode.com/gh_mirrors/st/streamlit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表