这个项目到底在解决什么问题先说个我最近一直在琢磨的场景。一个团队花了几个星期把一个 Web 应用部署到了 Firebase Hosting 上部署倒是挺顺的但真正头疼的是后续的回归验证。每次前端改了点交互逻辑、后端调了个接口都得打开浏览器手动把核心流程走一遍登录、跳转、填表单、等结果、看文案对不对、看控件响应是否正常。耽误时间不说人手动点极致容易漏掉一些不常触碰的分支路径。我想做的事情很明确把这一整套人在浏览器上重复操作再确认的过程变成用 Python 脚本模拟用户操作去自动执行再交给 pytest 去编排、断言、报告、成规模地跑。这个项目就是围绕RPA-Python 与 pytest-firebase-hosting 集成展开的——用 Python 写 RPA 式的流程操作脚本用 pytest 作为测试调度框架并且通过一个自研的 pytest 插件把 Firebase Hosting 的部署生命周期纳入测试编排最终落地成一套可持续运行的 Firebase Hosting 测试自动化工程。可能有人看到 RPA 这个词会觉得这是企业级流程机器人才干的事实际上这个项目里取的是它的核心思路由程序模拟真实用户在页面上进行点击、输入、等待、读取等操作。这和测试自动化天然互补区别只是 RPA 一般面向固定流程的长期执行而 pytest 面向带断言、带前置条件、带失败定位的系统化验证。把两者拼在一起解决的痛点非常实际如何让一个部署在 Firebase Hosting 上的应用每天都能自动验一验出了细节变化能第一时间发现。这个内容适合谁看一类是刚上手 Web 自动化测试的 Python 开发者另一类是使用 Firebase Hosting 做前端或全栈部署、但缺少端到端回归手段的团队。我会从架构思路、插件设计、核心实操到踩坑经验全部展开代码可以直接抄走改改用。技术选型为什么是 Python pytest Firebase2.1 技术栈拆解与选型权衡先说 Python 这一层。现在能做浏览器自动化的语言选择不少Node.js 配 Puppeteer 也很好但 Python 生态有它的优势pytest 的 fixture 机制、参数化、插件体系非常成熟写测试用例的体验是很多其他语言生态比不了的。再加上如果团队里已经有 Python 写的数据处理脚本或接口自动化用例那技术栈统一的好处就很明显——至少不用为了一个回归验证场景另起一套语言体系。浏览器自动化驱动这块我最终选的是 Playwright。对比一下主流的两个方案对比维度SeleniumPlaywright启动与清理需要显式管理 driver 和浏览器的生命周期封装完善启动、连接、关闭都内置等待机制依赖显式等待和 sleep写起来繁琐内置自动等待对 RPA 式流程更友好调试体验截图、日志能力一般内置 trace 日志、视频录制、定位器重试多浏览器支持老牌支持面广对 Chromium、Firefox、WebKit 的支持一致性好当然如果你已有的代码库是基于 Selenium 的也没有必要强行迁移但新项目我建议直接用 Playwright带自动等待的 API 能让流程脚本少掉一半的等待处理逻辑。Firebase Hosting 这一层的选择其实是跟着应用部署走的。它的定位是静态资源托管加轻量服务转发很多团队用 Cloud Functions 配合做服务端渲染或 API 转发。对这个项目来说Firebase 的工具链 CLI 本身支持命令行部署和回滚这给自动化提供了比较干净的接口。2.2 核心设计把 Firebase 部署生命周期pytest 化这是整个项目里最关键的一个设计决定。一开始我们很自然地会想到测试用例之前先去手动执行一次firebase deploy然后跑 pytest。但这个流程有几个问题一是容易忘二是多人协作时部署状态无法对齐三是如果想在测试结束时自动清理部署版本、恢复旧的托管版本手动操作根本做不到。我们这个项目要想真正自动化就不能把部署当作测试的外部依赖得把它塞进 pytest 的执行生命周期里。于是就有了标题里出现的pytest-firebase-hosting。这个词并不是某个公开的现成插件而是我们在项目里自研封装的一个 pytest 插件通过 pytest 的钩子函数在 session 级别完成部署 - 等待就绪 - 执行用例 - 清理的完整编排。它承担的任务包括用pytest_addoption接收预留的项目目录、Firebase 项目 id、部署阶段等参数在pytest_sessionstart阶段调用 Firebase CLI 触发部署并设置就绪等待在pytest_sessionfinish阶段执行清理或回滚操作通过 fixture 将部署后的站点地址注入测试用例避免各个用例各自找 base URL。这样设计的好处是对使用者来说跑一套回归只需要一条命令。部署、测试、清理的整个过程是同一个进程的生命周期状态不会错位。插件的价值不是省了一条终端命令而是把部署行为变成了可编排、可复跑、可对接持续集成的标准步骤。2.3 目录结构与用例组织方式目录结构直接决定了这套工程能撑多久。简单粗暴地把所有操作代码堆在 test 文件里前期很爽后期维护就是灾难。我用的结构是这样rpa_firebase_test/ ├── conftest.py # 全局 fixture 与插件注册 ├── firebase_plugin.py # 自研 pytest-firebase-hosting 的核心逻辑 ├── deploy_config.yaml # 部署参数独立于代码 ├── flows/ # RPA 流程脚本可以被多个用例复用的页面操作 │ ├── __init__.py │ ├── login_flow.py │ ├── order_flow.py └── tests/ ├── test_login_regression.py ├── test_checkout_flow.py └── test_navigation_baseline.py把流程操作和测试断言分开是非常重要的一步。如果每一条用例里都重复写打开页面 - 点击按钮 - 填写表单以后页面上任何一个按钮的定位方式变了你要改的地方可能就有十几处。抽成login_flow.py、order_flow.py之后改只改一处用例里剩下的就是走流程 验证关键状态。用例组织上我建议按业务链路从主到次排列核心购买流程优先登录注册排第二导航类冒烟排在后面。这样一旦出现全站不可用的情况前面几张用例会最先失败定位也更快。实操过程从零搭起一套可跑的测试工程3.1 环境准备与最小化配置环境上我建议直接用 Python 3.11 或更高版本避免一些类型注解兼容上的老问题。安装依赖pip install pytest playwright pyyaml playwright install chromiumfirebase-tools需要单独装注意它是 Node 工具链不是 pip 包npm install -g firebase-tools这里有个容易踩的坑Firebase CLI 直接执行部署时如果当前环境没有登录态会弹出浏览器登录流程这在自动化里是绝对不能接受的。所以需要提前生成一个 token 并通过环境变量传入CI 或者定时任务里都用同一套方案。export FIREBASE_TOKEN你的部署token这个 token 的权限要严格控制它是能往 Firebase 项目里发布内容的凭据不要混进普通开发环境或直接提交到仓库。用环境变量注入是最稳妥的姿势。deploy_config.yaml我写得很简单project_id: rpa-demo-project deploy_dir: dist deploy_only: hosting public: dist cleanup_after_session: true base_url_override: 这些参数后续会在插件里读到作为部署和 URL 注入的依据。把配置独立出来的动机很简单同一套代码可能要对着 staging、production 等不同环境跑硬编码在代码里就要反复改代码放在 YAML 里一个环境一份配置文件就行。3.2 写第一条自动化流程用例流程自动化测试用例和单元测试在思路上有本质差别。单元测试关注一个函数的返回是否符合预期RPA 式测试关注的是当用户在页面上做了一套操作之后系统呈现的状态是否符合预期。所以断言的目标往往是某个 URL 是否切换、某个元素是否出现、某段文本是否变化。我拿最典型的登录流程举例。假设这个应用登录成功后会跳转到控制台首页并且右上角出现用户名。流程脚本先抽出来# flows/login_flow.py from playwright.sync_api import Page def do_login(page: Page, email: str, password: str) - None: 执行完整登录流程打开首页 - 跳转登录页 - 填表 - 提交。 page.goto(/) page.get_by_role(link, name登录).click() page.get_by_label(邮箱).fill(email) page.get_by_label(密码).fill(password) page.get_by_role(button, name登录).click()然后用例里只需要做断言# tests/test_login_regression.py def test_login_success(page, firebase_base_url): do_login(page, demoexample.com, Passw0rd!) page.wait_for_url(**/dashboard) assert page.get_by_text(欢迎回来演示用户).is_visible()firebase_base_url这个 fixture 就是插件通过 session 部署完成后注入的。用例本身不需要知道 URL 是https://rpa-demo-project.web.app还是本地调试的http://127.0.0.1:5000它只需要相对路径操作URL 的差异全部由 fixture 处理。这套写法的自然读数换环境的时候只是换横向的配置不会碰用例内部。回归用例库可以跨环境复用。3.3 实现 pytest-firebase-hosting 插件的关键代码这里是整个项目的核心。插件不能太冗长封装到一个文件里几百行内搞定。核心逻辑我拆成四个部分。先看参数注册和会话开始时的部署# firebase_plugin.py import os import subprocess import time import yaml import pytest def pytest_addoption(parser): parser.addoption(--firebase-config, actionstore, defaultdeploy_config.yaml) parser.addoption(--skip-deploy, actionstore_true, help跳过部署步骤直接跑测试) pytest.hookimpl(tryfirstTrue) def pytest_sessionstart(session): if session.config.getoption(--skip-deploy): return cfg_path session.config.getoption(--firebase-config) with open(cfg_path, r, encodingutf-8) as f: cfg yaml.safe_load(f) env os.environ.copy() # 从环境变量读取部署 token避免出现在任何配置文件中 env[FIREBASE_TOKEN] os.environ[FIREBASE_TOKEN] cmd [firebase, deploy] if cfg.get(project_id): cmd [--project, cfg[project_id]] if cfg.get(deploy_only): cmd [--only, cfg[deploy_only]] if cfg.get(public): cmd [--public, cfg[public]] proc subprocess.run(cmd, capture_outputTrue, textTrue, envenv, timeout180) if proc.returncode ! 0: raise RuntimeError(fFirebase 部署失败: {proc.stdout} {proc.stderr})部署成功之后得把部署好的 URL 传给用例。这个可以通过自定义 fixture 实现插件文件里注册一个全局 fixturepytest.fixture(scopesession) def firebase_base_url(pytestconfig): if pytestconfig.getoption(--skip-deploy): from urllib.parse import urlparse return http://127.0.0.1:5000 with open(pytestconfig.getoption(--firebase-config), r) as f: cfg yaml.safe_load(f) return fhttps://{cfg[project_id]}.web.app部署之后 Firebase Hosting 并不会瞬间让新版本在全球缓存节点全部生效所以部署起来后必须等待就绪。插件里用一个简单的探测循环来替代固定 sleeppytest.hookimpl(trylastTrue) def pytest_sessionstart(session): # 接上面的部署逻辑... if proc.returncode ! 0: raise RuntimeError(Firebase 部署失败) url fhttps://{cfg[project_id]}.web.app import urllib.request for attempt in range(30): try: resp urllib.request.urlopen(url, timeout10) if resp.status 500: return except Exception: pass time.sleep(2) raise RuntimeError(部署完成但站点无法访问)这里的循环等待比固定time.sleep(10)更可靠部署时间随构建内容大小波动固定 sleep 要么造成浪费要么造成超时误判。循环探测是成本最低又最准确的方式。测试结束时还要做清理。默认的策略不是删除部署而是保留最后一次部署作为最新的线上版本。如果配置里cleanup_after_session为 true则调用回滚命令把版本恢复部署前的状态。回滚需要拿到之前的版本 ID这个可以在部署前先执行firebase hosting:channel:list或从部署日志解析。简化版实现如下pytest.hookimpl(trylastTrue) def pytest_sessionfinish(session, exitstatus): cfg_path session.config.getoption(--firebase-config) with open(cfg_path, r, encodingutf-8) as f: cfg yaml.safe_load(f) if not cfg.get(cleanup_after_session, True): return env os.environ.copy() env[FIREBASE_TOKEN] os.environ[FIREBASE_TOKEN] rollback_cmd [ firebase, hosting:versions:rollback, --project, cfg[project_id], --version, cfg.get(rollback_version, previous), ] subprocess.run(rollback_cmd, capture_outputTrue, textTrue, envenv, timeout120)回滚策略要注意如果测试环境本身就是要保留最新代码清理步骤可以不做cleanup_after_session设 false 即可。3.4 本地跑通与持续集成接入思路本地调试阶段的体验也很重要。我调试时通常不直接部署而是先把 Firebase 本地模拟器起起来用--skip-deploy跳过真实部署指向本地模拟器端口firebase emulators:start --only hosting pytest --skip-deploy这样本地改代码、跑用例、改用例的循环非常快不用每次都在公网部署一遍。等本地全绿了再做一次真实部署的完整流程。接入持续集成时关键是新增一个定时任务或者 PR 触发的 pipeline里面按顺序执行pip install -r requirements.txt playwright install chromium pytest -v --firebase-config deploy_config.yaml部署 token 通过 CI 的 secret 配置注入环境变量。注意 CI 环境下浏览器安装路径可能和本地不同Playwright 在 Linux CI 上可能还需要补装系统依赖playwright install-deps可以一键解决。实测中踩过的坑与排查方法4.1 部署还没就绪用例已经跑起来了这是第一次接入真实部署时最早遇到的问题。现象是部署命令返回成功紧接着第一条用例就开始跑但页面加载出来是旧版本或者干脆触发了 404。根因分析Firebase CLI 的deploy命令返回成功只代表它把文件上传到了目标服务不代表全球边缘节点都完成了版本切换。Hosting 的 CDN 更新是有传播时间的尤其当页面资源引用没有做 hash 版本管理时浏览器缓存更容易干扰。解决办法就是前文提到的就绪探测。探测目标不应该是静态页面本身最好是页面依赖的一个 API 地址或者一个带有版本号字符串的页面路径。我后面的做法是在部署后的根页面里藏一个版本号 meta 标签探测脚本直接检查meta namedeploy-version是否和本次部署的 commit hash 一致这样能确定线上真正切换到了新代码。4.2 元素定位不稳定等待策略怎么写才稳RPA 式脚本最常见的不稳定来源就是元素定位时机。新手容易直接click()结果按钮还没渲染完成脚本就报错。Playwright 的定位器有啥好处它的主体方法默认带 actionability 检查会自动等待元素可见、可点击但并不能覆盖所有场景。比如某个操作会触发接口请求然后页面上某个区域异步更新。这时候光靠元素可见还不够因为旧内容可能还在页面上。我的处理方式是对状态切换建立明确等待点# 等待某个异步任务完成的推荐模式 page.wait_for_response(**/api/order/status) # 或等待某个元素变化出现 page.get_by_text(订单处理完成).wait_for()不要动不动就time.sleep(5)等待固定时长既慢又容易带来随机失败。改成等待具体事件之后用例稳定性会大幅提升。4.3 本地通过、云端失败的典型差异有一类问题很典型本地用模拟器测所有用例都过了一部署到真实 Firebase Hosting 上就有几个用例失败。排查下来大多是这几种原因本地是 localhost没有 CDN 缓存问题云端则是边缘节点 浏览器缓存旧资源未失效云端的域名是https://*.web.app如果代码里有绝对 URL 或者跨域配置就会出现本地没有的 CORS 问题Firebase Hosting 的 rewrites 规则如果在本地模拟器和云端不完全一致某些路由行为会有差异。这类问题的排查方法确保本地模拟器使用的firebase.json和云端保持一致部署完成后不要直接刷新浏览器看新页面而是在无痕窗口里验证如果怀疑是 CDN 导致的可以再发一次空提交强制重新部署。4.4 常见问题速查表问题可能原因排查与解决部署成功但用例访问旧页面CDN 传播或缓存就绪探测改为校验版本号清理浏览器缓存等待若干秒再进入用例登录流程中元素定位超时异步加载没有稳定等待点改用wait_for_response或expect的轮询机制避免固定 sleep本地全通过部署后失败本地模拟器配置与云端不一致对同一份firebase.json检查 rewrites/headers用无痕窗口复测点击按钮后没有任何响应前端 JS 报错开启 Playwright 的 trace 记录回放一步一帧看控制台报错部署 token 无效token 过期或权限被收重新生成部署 token 并严格给最小权限并发执行多条用例时互相干扰共享同一个用户会话每个用例独立浏览器上下文或者使用独立测试账号并隔离 localStorage让这套工程更好用的几个进阶技巧5.1 测试数据与用例彻底分离回归测试里最让人头疼的往往是数据环境。如果登录用例写死了某个邮箱而这个账号在测试环境被人手动改过密码测试就崩了可代码本身没问题。这种问题用上 fixture 的参数化和 conftest 里的数据读取就能解决。我把测试账号和密码放到单独的 YAML 文件不在用例代码里出现具体值用 fixture 读取并返回给用例# conftest.py pytest.fixture def test_account(): with open(test_accounts.yaml, r, encodingutf-8) as f: data yaml.safe_load(f) return data[main_user]这样权限控制也更清晰存放测试账号的配置文件设置成本地读取权限最小化避免被误提交或误传播。5.2 失败自动截图与诊断信息收集流程测试一跑就是几十分钟半夜定点跑失败了一次等你早上看到报告时页面状态可能已经变了手动复现也未必能重现。这种情况下失败时刻的现场信息是最宝贵的。我在 conftest 里加了失败截图钩子pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: page item.funcargs.get(page) if page: timestamp int(time.time()) page.screenshot(pathfartifacts/failure_{item.name}_{timestamp}.png, full_pageTrue)截图之外Playwright 的 trace 功能强烈建议开启它能记录整个会话的 DOM 快照和网络请求回放的时候能看到哪一步开始出错。trace 开启有性能开销建议通过命令行参数控制只在出问题时打开。5.3 流程脚本与业务逻辑的复用边界做了一段时间这套自动化之后我最大的体会是要理清流程脚本的复用边界。do_login、add_to_cart、submit_order这类操作是稳定的、值得抽出来的但如果你发现为了复用一段流程不断给它加各种 flag 参数来适配不同场景那就不对劲了——说明它在承担太多职责拆分比强行复用更好。复用的标准是一个流程脚本只描述一条清晰的操作链路断言逻辑绝不混进去。比如do_login只负责到达登录后页面这个状态至于页面上到底该出现什么、不该出现什么是用例层负责的事。把操作和断言分离之后流程脚本作为稳定基础层可以被大量用例共享而且不会因为某个业务规则变化而连锁破坏。我实际跑下来还有一个经验不要一开始就追求把所有业务链路全部自动化。先选几条最高价值的核心链路做通等证明这套工程稳定可靠、维护成本可控之后再逐步加用例。自动化测试工程一旦做得太重、用例太碎失败的噪音会让人失去维护的信心。有价值的不是用例数量而是每一条用例是否能在足够长的时间里稳定地守护住关键业务。最后再分享一个实践细节对于 Firebase Hosting 的自动化验证我把这个工程接到了某个聊天通知频道里让定时任务跑完后的失败摘要自动推送到团队群。这样出了问题不用等谁手动跑一遍才能发现往往是用户还没感知我们已经拿着失败截图在排查了。自动化测试的最高价值不在证明没坏而在尽快发现坏了——这一套跑起来之后整个回归流程从原来的一小时人工操作压缩到了十几分钟全自动完成。