Streamlit E2E 测试指南:基于 Playwright 与 pytest 的全栈黑盒测试实践
发布时间:2026/9/19 11:51:02 作者:尧图编辑部 阅读量:1,286

Streamlit E2E 测试指南基于 Playwright 与 pytest 的全栈黑盒测试实践【免费下载链接】streamlitStreamlit — A faster way to build and share data apps.项目地址: https://gitcode.com/gh_mirrors/st/streamlitStreamlit 的端到端E2E测试体系位于仓库的 e2e_playwright 目录采用Playwright pytest组合以用户视角黑盒验证由前端、后端、通信、状态与视觉表现组成的完整 Streamlit 系统。本文以 e2e_playwright/AGENTS.md由 e2e_playwright/CLAUDE.md 通过./AGENTS.md指令引入为骨架结合 e2e_playwright/conftest.py 等源码实现系统讲解 E2E 测试的目录结构、核心 Fixture 与工具、外部应用测试模式、URL 处理规范、编写与运行测试的最佳实践以及截图快照的更新流程。读完后你将能够为新的 Streamlit 元素编写可复用的 E2E 测试并在本地、CI 及外部托管应用三种环境下正确运行与维护它们。E2E 测试在 Streamlit 开发中的定位E2E 测试与 Python/JS 单元测试互为补充。单元测试更快聚焦内部逻辑、输入输出校验与特定消息序列而 E2E 测试验证的是完整的 Streamlit 系统——从前端渲染、后端执行到 WebSocket 通信、状态管理乃至视觉外观。因此当需要测试依赖全栈行为或需要视觉验证的功能时应使用 E2E 测试尤其是新增元素或对既有元素做重大改动时。前端 (frontend/) ── WebSocket ── 后端 (lib/streamlit/) ── 应用脚本 (*.py) │ │ └───────── 视觉快照断言pixelmatch 像素比对这一取舍逻辑决定了测试编写方式E2E 测试成本高昂应一次浏览器运行换取最大置信度而非像单元测试那样追求细粒度覆盖。详细的运行与实现文档可参考 wiki/running-e2e-tests.md。测试结构与组织约定E2E 测试全部位于e2e_playwright/目录每个测试由两个文件成对组成文件作用*.py被测的 Streamlit 应用脚本如st_dataframe.py*_test.pyPlaywright pytest 测试文件负责启动并测试上述应用如st_dataframe_test.py运行测试时pytest 文件会自动执行对应的 Streamlit 脚本——这一映射由 conftest.py 中的resolve_test_to_script完成它把测试模块文件名中的_test.py替换为.py从而定位被测脚本。若测试针对某个具体的 Streamlit 元素文件名应以st_element_name为前缀例如 st_button.py 与 st_button_test.py便于按元素检索和组织。测试产物遵循固定目录约定所有截图快照存放在e2e_playwright/__snapshots__/os/其中os为运行平台如darwin、linux因为不同操作系统会产生略有差异的渲染结果其他 E2E 测试结果存放在e2e_playwright/test-results/下e2e_playwright/test-results/test_name/失败测试相关的视频与 tracee2e_playwright/test-results/snapshot-tests-failures/os/test_script/test_name/失败快照测试的期望图、实际图与差异图e2e_playwright/test-results/snapshot-updates/os/test_script/test_name/本次运行中更新过的全部截图。核心 Fixtures 与工具方法测试所需的大部分基础设施由 conftest.py 提供直接从conftest.py导入即可使用Fixture / 工具类型说明app: Pagefixture以亮色主题加载被测应用themed_app: Pagefixture以亮色与暗色两种主题加载应用由app_theme参数化驱动见 conftest.pyapp_target: AppTargetfixture应用交互包装器抽象了应用 DOM 位于顶层Page还是宿主页FrameLocator的差异支持 iframe 托管的扩展应用app_base_url: strfixture应用导航的基础 URL外部地址或 localhost见 conftest.pybuild_app_url(...)函数URL 构造器基于app_base_url组合 path/query/fragmentassert_snapshotfixture截图测试对 locator 元素截图并做像素比对调用元素前需确保其渲染稳定wait_for_app_run(app)函数等待一次应用运行结束wait_for_app_loaded(app)函数等待应用首次加载完成rerun_app(app)函数触发一次应用重跑并等待完成wait_until(app, fn)函数循环执行测试函数直至返回True或超时app_with_query_paramsfixture以一组预配置的查询参数加载应用等待语义的源码实现等待逻辑是 E2E 测试稳定性的基石。wait_for_app_run 的实现揭示了它依赖的前端状态信号先等待initial_wait默认 210ms兼容具有防抖超时的组件如 pydeck 图表有 200ms 防抖等待[data-testidstApp][data-test-connection-stateCONNECTED]出现确保 WebSocket 连接建立等待[data-testidstApp][data-test-script-statenotRunning]即脚本运行状态从initial→running→notRunning走完断言stSkeleton元素数量为 0确保所有元素已完成渲染最后再等待wait_delay默认 100ms让页面完全绘制。wait_until 则是expect无法覆盖的场景下的通用轮询工具以interval默认 100ms为周期重复调用回调直到返回True/None或超过timeout默认 5000ms回调抛出的AssertionError会被吞掉并继续轮询直至超时后以TimeoutError失败。外部测试模式External Test Mode默认情况下E2E 测试会在本地为对应测试脚本启动一个 Streamlit 服务器。但在某些环境中你可能希望针对已运行的应用执行测试例如应用托管在 SSO 之后或嵌入在宿主页面中如 Snowsight。启用方式与参数外部模式通过 CLI 选项或环境变量配置两者取其一CLI 优先。这些选项在 pytest_addoption 中注册CLI 选项环境变量说明--external-app-urlSTREAMLIT_E2E_EXTERNAL_APP_URL外部托管应用的绝对 HTTP(S) URL不启动本地服务器--external-host-urlSTREAMLIT_E2E_EXTERNAL_HOST_URL将应用嵌入 iframe 的宿主页面绝对 HTTP(S) URL不启动本地服务器需与--external-app-url同时配置--external-iframe-selectorSTREAMLIT_E2E_EXTERNAL_IFRAME_SELECTOR宿主页面中 iframe 元素的 CSS 选择器默认iframe--browser-state-pathSTREAMLIT_E2E_BROWSER_STATE_PATHPlaywright storage state JSON 文件路径可用于预置已认证的浏览器会话当外部应用模式启用时只有标记了pytest.mark.external_test的测试会被执行其余测试全部跳过——这一逻辑由 autouse 的 skip_non_external_tests_in_external_mode fixture 实现。此外pytest_collection_modifyitems 会在收集阶段做校验若开启了外部模式但没有任何测试被标记直接抛出UsageError提示用户避免静默全跳过的困惑。external_test 标记的参数pytest.mark.external_test仅支持少量关键字参数完整的规范列表见 conftest.py也可通过pytest --markers查看upload_test_assets布尔值默认False当外部托管应用需要访问e2e_playwright/static/目录时设为True。标记不接收位置参数未知关键字参数会触发UsageError如pytest.mark.external_test(upload_test_assetsTrue)是合法用法。需要说明的是是否将测试标记为external_test目前需要手动判断仅当测试明确设计为针对外部托管应用/宿主页面运行时才添加该标记。AppTarget 抽象编写external_test测试时应优先使用app_targetfixture 而非裸的Page/FrameLocator。AppTarget 是一个冻结数据类集中封装并隐藏了应用 DOM 在哪里的细节page顶层 Playwright Page用于路由、事件、重载、超时、trace 等dom应用选择器的执行上下文本地模式为Page外部宿主模式为FrameLocatorbase_url应用自身的规范基础 URL不是宿主页面modelocal | external_direct | external_host三种运行模式。它提供了locator()、get_by_test_id()、get_by_role()、get_by_text()等与 Playwright 一致的接口以及wait_for_run()、wait_for_loaded()方法使测试代码无需按本地/iframe 分支编写。从源码结构看这一抽象正是为了把 iframe 与顶层页面的差异收敛为测试基础设施的内部实现细节。外部测试的适用性判断从文档与实现可归纳出一个适合标记为external_test的测试通常具备以下特征可以在不启动本地应用服务器的情况下运行不依赖测试模块的*.py脚本由测试框架拉起使用稳定的选择器以及app_target/app_base_url抽象保证顶层与 iframe 托管两种场景都能工作避免依赖本地专有基础设施如请求路由/拦截iframed_app或与文件系统耦合的假设。URL 处理规范不硬编码 localhost在测试模块conftest.py之外中严禁硬编码或拼接http://localhost:{app_port}。正确做法是统一使用app_base_url与build_app_url# 导航优先 page.goto(build_app_url(...))而不是字符串拼接 page.goto(build_app_url(app_base_url, path/page, query{key: value}, fragmentsection)) # 网络拦截与直接 HTTP 调用同样基于 app_base_url 构造 page.route(build_app_url(app_base_url, path/media/**), handler) page.request.get(build_app_url(app_base_url, path/_stcore/health))build_app_url 的实现保证了保留基础路径base path即使传入以/开头的 path 也会拼接到基础路径之后如base_urlhttps://host/prefix时path/_stcore/health得到/prefix/_stcore/healthquery 支持dict或a1b2字符串两种传入方式且会与 base URL 中原有的 query 合并fragment 接受foo或#foo两种写法。同时要避免通过解析当前 URL 来恢复 localhost 端口如urlparse(app.url).port需要稳定基址时一律使用app_base_url或app_target.base_url。这套规范的目的是让同一份测试代码既能在本地运行也能无缝切换到外部应用模式。测试资源管理禁用外部 URL测试中应避免引用外部 URL第三方依赖因为它们可能引发网络波动或直接导致测试中断。仓库为此提供了统一的本地资源约定Python API 资源如st.audio、st.video、st.image通过文件路径从e2e_playwright/static/加载URL 引用将文件放入e2e_playwright/static/以./app/static/filename作为相对 URL 使用。可参考 st_video.py 和 st_image.py 中的既有写法。static/目录中还预置了sample.pdf、cat.jpg、sintel-short.mp4等各类媒体素材覆盖音频、视频、图片、PDF 等多种测试场景。编写测试的最佳实践E2E 测试的运行成本远高于单元测试因此优化的核心目标是每次浏览器运行的置信度最大化成本控制与组织策略每个方面只测试一次优先使用聚合场景测试而非大量微测试新测试若与既有测试文件范围匹配优先加入现有文件避免为小改动新建测试脚本将相关测试分组到单一、逻辑完整的测试文件中如按 widget 或功能分组因为 CI 中每个独立测试文件都需要启动一个新的 Streamlit 应用服务器多个测试共享相同 setup 与页面状态时优先采用单个聚合场景只加载一次应用、按顺序覆盖多个断言减少浏览器加载次数谨慎使用pytest.mark.parametrize避免对昂贵流程做参数化当 setup 主导运行时间时优先在单个场景测试内迭代变体。断言与等待使用expect而非assertexpect具备自动等待能力可显著降低 flakinessassert没有自动重试容易因时序问题产生偶发失败若expect不够用使用wait_until工具wait_for_timeout应尽量避免仅在确有特定用途时使用断言应模拟用户与 UI 交互的真实方式。元素定位器优先级优先使用基于 label 或 key 的定位器而非基于索引的访问如get_by_test_id().nth(0)。推荐优先级如下按 label 获取元素见 app_utils.py 中的get_text_input、get_button、get_slider等方法不支持label但支持key的元素通过唯一 key 获取get_element_by_key若元素既不支持 label 也不支持 key可用st.container(keymy_key)包裹再通过get_element_by_key(my_key).get_by_test_id(stComponent)定位。同时遵循以下选择器规范优先使用稳定定位器get_by_test_id、get_by_text、get_by_role而不是通过.locator使用 CSS/XPath当目标文本是页面其他文本的前缀或子串时必须配合exactTrue使用get_by_text()与filter(has_text...)。例如get_by_text(Count:, exactTrue)可避免误匹配 Count: 5——不加exactTrue时 Playwright 按子串匹配多个元素命中会触发 strict mode 违规尽量利用共享的 app_utils 工具方法从e2e_playwright.shared.app_utils导入。负面断言与交互多样性每个场景尽可能添加至少一条必须不发生must NOT happen的检查在验证正向 UI 结果的同时断言某个可能的回归不会出现例如不出现重复元素、tooltip 在 hover 前不显示、disabledTrue时 widget 保持不可交互、不发生意外的 rerun/状态变化等负面检查要高信号E2E 成本昂贵每个场景优先做一个有针对性的负面断言而不是堆砌大量负面矩阵让测试混合不同的交互方式如输入框交替使用 fill 和 type以获得更高的覆盖度。截图细节最小化截图面积优先截取特定元素而非整页除非确有必要主容器内的元素截图高度应控制在 640px 以下700px 屏幕高度减去 60px 头部避免被顶部 header 裁切命令相关快照的命名约定为st_command-test_description为测试起描述性名称。编写测试时应覆盖的常见场景为某个元素新增或修改测试时文档明确要求覆盖以下维度视觉Visuals为 normal 与disabled两种状态各做快照测试交互Interactivity测试用户交互并验证应用状态或输出如通过st.write写入的文本可借助 app_utils.py 中的expect_markdown等辅助函数常见上下文Common Contexts验证元素在st.fragment内和st.form内的行为核心行为Core Behavior状态持久化元素被临时卸载再重新挂载后widget 值应保留disabledTrue时元素不可交互若元素使用help参数验证 hover 时 tooltip 正确出现若元素使用key参数验证对应的 CSS class 或属性已设置若元素是 widget验证提供key后其身份保持稳定自定义配置Custom Config需要特定 Streamlit 配置选项的测试使用pytest.mark.early标记的模块级 fixture既有测试兼容性向既有应用脚本新增元素时检查现有测试是否断言了元素数量如to_have_count(18)并同步更新为新总数。其中pytest.mark.early的作用在 conftest.py 中有明确实现pytest 收集时会通过reorder_early_fixtures将这些 fixture 提前到执行顺序最前从而允许在应用初始化之前修改配置。本地运行、调试与快照更新运行测试运行命令由 Makefile 提供所有_test.py文件也会在 CI 的每个 Pull Request 上自动执行# 运行单个测试文件 make run-e2e-test e2e_playwright/name_of_the_test.py # 运行单个测试用例 make run-e2e-test e2e_playwright/name_of_the_test.py::test_name # 传入任意 pytest 参数通过 PYTEST_ADDOPTS 环境变量 PYTEST_ADDOPTS-k test_name -vvv make run-e2e-test e2e_playwright/name_of_the_test.py注意两点若修改了前端逻辑需先运行make frontend-fast更新前端产物否则 E2E 测试可能使用旧的前端资源conftest.py 中 404 提示也印证了这一点应用返回 404 时会提示尝试构建前端快照缺失或不匹配的错误可以暂时忽略这些快照需要手动更新。本地运行依赖make目标内部通过uv run pytest执行并默认使用pytest-playwright提供的浏览器实例。测试服务器由app_serverfixture 通过 start_app_server 启动它会以streamlit run拉起被测脚本并传入--server.headless true、--global.e2eTest true、--server.fileWatcherType none等一系列测试专用配置启动失败时自动重试最多 3 次。调试测试Makefile 提供交互式调试模式基于 Playwright 的 debug 模式包含 headed 浏览器与 Inspectormake debug-e2e-test e2e_playwright/name_of_the_test.py其他常用调试手段# 慢速模式 视频录制在 e2e_playwright 目录下直接执行 cd e2e_playwright uv run pytest name_of_the_test.py -s --video on --slowmo 500 # headed 模式打开真实浏览器窗口观察测试执行 # 可与 --slowmo 组合便于在 DevTools 中检查元素快照更新截图测试默认只针对本地操作系统如e2e_playwright/__snapshots__/darwin/生效因为 CI 运行在 Ubuntu 上可能产生略有差异的快照。本地更新截图的方式删除e2e_playwright/__snapshots__/os/下的过期快照运行make update-snapshots一次首次运行可能因缺少截图而失败这是预期行为。此外仓库提供自动化的快照更新脚本make update-snapshots对应 scripts/update_e2e_snapshots.py其流程为匹配本地分支对应的 PR → 等待该 PR 最新的 Playwright E2E 工作流运行完成 → 将 CI 生成的更新快照同步到本地仓库。如需只更新改动过的文件可用make update-snapshots-changed。同步后应只提交预期更新的快照警惕 flaky 测试导致的无关联快照变化。assert_snapshot 的参数与阈值assert_snapshot 提供像素级比对能力核心参数如下参数默认值说明element必填待截图的ElementHandle、Locator或Pagename测试函数名快照文件名不含扩展名可附加参数化后缀image_threshold0.002允许差异像素占总像素的比例微小图会取max(1, ...)保证非零阈值pixel_threshold0.05传给 pixelmatch 的逐像素比较阈值0.0–1.0不允许超过 0.10否则比对过于宽松fail_fastFalse遇到第一个像素差异即停止比对file_typepng快照格式支持png/jpgjpg 使用 quality90show_app_headerNone是否显示应用 header对非 Page 元素默认隐藏 header通过透明化.stAppHeader背景实现其实现逻辑值得注意快照缺失时不会立即失败而是记录错误并继续以便一次运行生成全部缺失快照快照不匹配时会生成diff_*、actual_*、expected_*三张图写入snapshot-tests-failures目录并且只有最后一次 rerun 才更新快照并失败避免与 rerunfailures 插件冲突导致 flaky 追踪失真。三条规则与减少 flakiness 的策略Three Rules of Playwright仓库将 Playwright 使用经验浓缩为三条规则尽可能使用expect断言assert缺乏自动等待是 Playwright 测试 flakiness 的主要来源之一尽可能使用get_by_test_id定位元素仅当目标无法通过 test-id 访问时才使用.locator不要用assert若expect无法覆盖你的场景改用wait_until工具方法。减少 flakiness 的通用手段在交互元素上执行操作之前额外添加expect(element).to_X检查通常能显著降低 flakiness。增加或调整超时有时也能缓解但更多时候这是一种掩盖问题的反模式。当无法找到直接修复方案、且 flakiness 并不指向可复现的 bug 时pytest.mark.skip_browser当 flakiness 仅出现在特定浏览器时跳过该浏览器pytest.mark.flaky(reruns3)显著降低测试导致 CI 失败的概率。其他实用技巧测试与特定浏览器不兼容时使用pytest.mark.skip_browser(firefox)装饰器跳过assert_snapshot是非等待断言若元素尚未完全加载会引入 flakiness调用前应根据情况先做等待检查不要在测试中使用wait_for_timeoutPlaywright 官方文档明确生产环境禁止使用等待时间类的测试天然 flaky应改用自动等待的定位器操作与断言需要特定 Streamlit 配置选项时可在测试文件中添加pytest.mark.early标记的模块级 fixture 来注入环境变量import os import pytest pytest.fixture(scopemodule) pytest.mark.early def configure_options(): Configure Streamlit config options. os.environ[STREAMLIT_SERVER_MAX_MESSAGE_SIZE] 3 yield del os.environ[STREAMLIT_SERVER_MAX_MESSAGE_SIZE] def test_something(app: Page, configure_options): # Test code测试结果归档与 CI 集成CI 中的 Playwright E2E Tests workflow 运行完成后测试结果会上传为 workflow artifacts可从工作流运行摘要的 Artifacts 部分访问。playwright_test_results文件夹仅在测试失败时上传包含视频、差异截图以及snapshot-updates文件夹内含本次运行更新的全部截图——后者是 CI 环境下更新快照的主要途径删除本地过期快照 → 推送改动 → CI 完成后从 artifacts 取回更新截图 → 确认无误后推回分支。本地失败测试的结果则位于e2e_playwright/test-results/包括视频、trace、差异截图与更新截图按测试名与平台分目录存放。此外conftest 中通过playwright_profilingfixture 对受支持浏览器自动进行性能测量可用pytest.mark.no_perf标记跳过并注册了e2e_playwright.shared.stats_reporter作为 pytest 插件。小结Streamlit 的 E2E 测试体系是一套成熟的应用脚本 测试文件双文件范式通过 conftest.py 集中提供服务器管理、URL 构造、等待语义与快照比对等基础设施通过AppTarget抽象屏蔽本地与 iframe 托管的差异并以外部的external_test标记将测试扩展到 SSO 等真实托管环境。对开发者而言最需要牢记的三件事是用expect代替assert、用build_app_url代替 localhost 硬编码、用聚合场景代替微测试。遵循 e2e_playwright/AGENTS.md 与 wiki/running-e2e-tests.md 中的约定即可为 Streamlit 的每个新元素写出稳定、高效且可持续维护的全栈回归测试。【免费下载链接】streamlitStreamlit — A faster way to build and share data apps.项目地址: https://gitcode.com/gh_mirrors/st/streamlit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考