UI 自动化框架工程化:从能跑到 CI 可用的四道坎

框架自己跑通只要一天,进 CI 之后才会暴露真问题。以下四道坎,都是我在企业能源监测与政务项目里真实踩过的。

一、元素定位:不要和脆弱的选择器死磕

最初的冒烟用例直接打真实站点首页,用 CSS 选择器填搜索框。本地跑绿的,CI 上隔天就红——对方改版,输入框还在但不可见,fill 直接超时 30 秒。

结论很直接:CI 冒烟不要依赖第三方页面。演示性质的搜索流程改用仓库内静态页 assets/demo_search.html,行为完全由本地 JS 决定,既保留了 Page Object + 数据驱动的教学结构,又让 CI 结果确定。真实站点的用例留给本地调试跑。

二、数据驱动:用例与数据分离

把关键词与期望值抽到 YAML,用例只负责调度:

# TestDatas/search_data.yaml
- keyword: "Playwright"
  expected: "Playwright"
- keyword: "Allure"
  expected: "Allure"
@pytest.mark.parametrize("case", load_yaml("search_data.yaml"))
def test_search_flow(self, page, case):
    demo = DemoSearchPage(page)
    demo.open_demo_page()
    demo.search(case["keyword"])
    assert case["expected"] in demo.get_first_result()

新增场景只加 YAML,不用碰代码——这是回报率最高的一步。

三、失败取证:让 CI 失败可定位

CI 上最烦的不是失败,是"不知道为什么失败"。加了一个全局 fixture,用例失败时自动截图并打印页面 URL:

@pytest.fixture(scope="function", autouse=True)
def attach_on_failure(request):
    yield
    if not getattr(request.node, "rep_call_failed", False):
        return
    if "page" not in request.fixturenames:   # 纯逻辑单测不触发浏览器
        return
    page = request.getfixturevalue("page")   # 懒加载,避免拖起浏览器
    ...

这里有个隐蔽的坑:最初写成 def attach_on_failure(page, request)——autouse fixture 的参数会被立即解析,于是每一个纯单元测试都被迫启动浏览器,没装 chromium 的环境直接全挂。改成懒加载后才真正做到"UI 用例取证、单测零依赖"。

CI 侧把 Reports/screenshots/ 作为 artifact 上传,保留 7 天。

四、有头/无头:别让 CI 猜

浏览器参数改为环境变量注入,本地有头调试、CI 无头跑:

HEADLESS = os.getenv("HEADLESS", "1").lower() in ("1", "true", "yes")

@pytest.fixture(scope="function")
def browser(browser_type):
    browser = browser_type.launch(
        headless=HEADLESS,
        slow_mo=int(os.getenv("SLOW_MO", "0")),
        args=["--disable-dev-shm-usage"],   # CI 容器 /dev/shm 默认 64M,不加会崩
    )

门禁怎么拆

三段式 CI,各管一件事,失败信息才清晰:

Job命令装什么
自测pytest tests/不需要浏览器
设备集成pytest test_mqtt/Mosquitto 服务
UI 冒烟pytest test_ui/test_case/playwright install --with-deps chromium

一个附带的通用教训

设备集成 job 一直失败,日志显示 is_connected=False。两个根因叠在一起:

  • 端口抢占:apt install mosquitto 会顺带拉起系统服务占住 1883(且默认拒绝匿名),自定义 broker 启动失败,客户端连到的是系统服务 → 先 systemctl stop mosquitto。
  • 握手竞态:paho 的 CONNACK 由网络线程异步处理,connect() 返回时连接未必已建立,本地毫秒级延迟掩盖了问题,CI 高负载下必现 → connect() 内部轮询等待连接确认再返回。

这两个 bug 的共同点:本地必绿、CI 必红。凡是这样,就该怀疑并发与时序,而不是环境。