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 必红。凡是这样,就该怀疑并发与时序,而不是环境。