1. 项目概述:当UI测试遇上“Midscene”
做UI自动化测试的朋友,这几年应该都经历过一个共同的痛点:UI变了,测试脚本就得跟着改,而且往往是牵一发而动全身。一个按钮的ID变了,一个弹窗的层级调整了,或者整个页面的布局重构了,之前辛辛苦苦写好的定位脚本可能就全废了,维护成本高得吓人。我自己带团队做自动化测试项目,最头疼的就是每次产品迭代后的脚本“灾后重建”工作。直到我开始接触并实践一种被称为“Midscene”的UI自动化测试思路,情况才发生了根本性的转变。这不仅仅是一个新工具,更是一种设计范式的革新。
简单来说,“Midscene”是一种基于“场景中间件”或“中间状态”理念构建的UI自动化测试框架。它的核心思想是,将测试脚本与具体的UI元素实现细节进行解耦。我们不再直接去定位那个ID为submit-btn的按钮,而是去描述“提交订单”这个业务场景。至于这个场景在当前版本的应用中,是由哪个按钮、在哪个位置、以何种样式呈现的,则由一个独立的“场景描述层”或“AI驱动层”来动态适配和管理。从网络热词中频繁出现的“Midscene”、“AI自动化测试”、“python playwright midsenc.js”等组合来看,这正是当前业界探索的热点方向——利用AI能力去理解UI,让测试脚本变得更“智能”和“健壮”。
这套方法特别适合谁呢?首先是面临频繁UI迭代的团队,比如处于快速成长期的互联网产品、使用Avalonia UI等跨平台框架的项目,或者像Vue、Angular这类前端框架常伴随样式和组件更新的场景。其次,是那些希望提升自动化测试脚本稳定性和可维护性的测试开发工程师。最后,对于想要探索下一代测试工具,将AI应用于质量保障领域的先行者来说,“Midscene”提供了一个非常务实的切入点。它解决的,正是传统基于坐标或固定属性定位(如Selenium、Appium)的脆弱性问题,让自动化测试能真正跟上敏捷开发的步伐。
2. 核心设计思路:解耦、描述与智能适配
为什么传统的UI自动化测试会如此脆弱?根本原因在于脚本与UI细节的“硬编码”绑定。我们的脚本里写满了driver.find_element(By.ID, “userName”)这样的语句。一旦前端开发把id=”userName”改成># 1. 创建项目目录并初始化 mkdir midscene-ui-autotest && cd midscene-ui-autotest python -m venv venv # 创建虚拟环境 # Windows: venv\Scripts\activate # Mac/Linux: source venv/bin/activate # 2. 安装核心依赖 pip install playwright pytest pytest-playwright # 自动化测试核心 pip install opencv-python pillow pytesseract # AI视觉相关(备用方案) playwright install # 安装浏览器驱动 # 3. 安装Tesseract OCR引擎(系统级) # Ubuntu/Debian: sudo apt-get install tesseract-ocr # Mac: brew install tesseract # Windows: 从 https://github.com/UB-Mannheim/tesseract/wiki 下载安装器 # 安装后,可能需要将安装目录(如C:\Program Files\Tesseract-OCR)添加到系统PATH
项目目录结构规划如下,这体现了关注点分离:
midscene-ui-autotest/ ├── pages/ # 页面对象模型 (POM),对应业务场景层的一部分 │ ├── __init__.py │ ├── login_page.py # 登录页面对象 │ └── dashboard_page.py # 仪表盘页面对象 ├── midscene/ # Midscene映射层核心 │ ├── __init__.py │ ├── locator_registry.py # 定位器注册表(核心映射库) │ └── ai_fallback.py # AI备用定位策略 ├── tests/ # 测试用例(业务场景层) │ ├── __init__.py │ └── test_login.py ├── conftest.py # Pytest配置,初始化Playwright └── requirements.txt3.2 构建核心:Midscene定位器注册表
这是“场景映射层”的心脏。我们创建一个locator_registry.py,它管理所有UI元素的定位策略。
# midscene/locator_registry.py class LocatorRegistry: """ 定位器注册表。存储业务逻辑元素名到具体定位策略的映射。 策略可以是Playwright选择器,也可以是AI定位函数。 """ def __init__(self, page): # page是Playwright的Page对象 self.page = page self.registry = {} self._init_registry() def _init_registry(self): """初始化注册表。这里硬编码了映射关系,实际中可以来自JSON/YAML文件或数据库。""" # 格式: ‘业务元素名’: {‘primary’: 主定位器, ‘fallback’: 备用AI定位函数} self.registry = { “login_page.username_input”: { “primary”: “#username”, # 主定位策略:CSS选择器 “fallback”: “locate_by_placeholder_and_label” # 备用AI策略名 }, “login_page.password_input”: { “primary”: “input[type=‘password’]”, “fallback”: “locate_by_placeholder_and_label” }, “login_page.submit_button”: { “primary”: “button:has-text(‘登录’)”, # Playwright的文本选择器 “fallback”: “locate_button_by_text” }, “dashboard.welcome_text”: { “primary”: “.welcome-message”, “fallback”: “locate_text_by_ocr” } } def get_locator(self, element_key: str): """根据元素键获取Playwright Locator对象。实现主定位失败时尝试备用AI策略。""" if element_key not in self.registry: raise KeyError(f“Element key ‘{element_key}’ not found in registry.”) strategy = self.registry[element_key] primary_locator = strategy.get(“primary”) # 首先尝试主定位器 locator = self.page.locator(primary_locator) if locator.count() > 0: # 简单检查元素是否存在 return locator else: # 主定位器失败,触发AI备用策略 print(f“Primary locator for ‘{element_key}’ failed. Attempting AI fallback...”) fallback_func_name = strategy.get(“fallback”) if fallback_func_name: # 这里假设有一个AI_Fallback类,下一节实现 from .ai_fallback import AI_Fallback ai = AI_Fallback(self.page) fallback_func = getattr(ai, fallback_func_name, None) if fallback_func: # AI函数应返回一个Playwright Locator或坐标 result = fallback_func(element_key) if result: # 如果AI返回的是坐标,可以转换为点击动作,这里简化处理 # 更佳实践是AI函数也返回Locator return result # 假设AI函数返回了定位到的Locator # 如果AI也失败,则抛出异常或返回主定位器(让其自然失败) print(f“AI fallback also failed for ‘{element_key}’. Using primary locator which may fail.”) return locator # 返回可能无效的主定位器,让后续操作抛出清晰错误这个注册表的关键在于,测试脚本只使用“login_page.username_input”这样的业务键。至于它对应#username还是[name=‘user’],由注册表决定。当#username失效时,系统会自动尝试调用AI函数locate_by_placeholder_and_label去寻找“可能”是用户名输入框的元素。
3.3 实现AI备用定位策略
接下来,我们在ai_fallback.py中实现几个简单的AI备用策略。请注意,这是简化示例,真实环境的AI定位要复杂和严谨得多。
# midscene/ai_fallback.py import cv2 import numpy as np from PIL import ImageGrab import pytesseract from playwright.sync_api import Page import time class AI_Fallback: def __init__(self, page: Page): self.page = page def locate_by_placeholder_and_label(self, element_key: str): """ 尝试通过OCR识别输入框附近的标签文本来定位元素。 适用于输入框的placeholder或相邻的<label>文本变化不大的情况。 """ # 1. 截取当前屏幕 screenshot_path = f“temp_screenshot_{int(time.time())}.png” self.page.screenshot(path=screenshot_path, full_page=True) img = cv2.imread(screenshot_path) # 2. 使用OCR识别屏幕上的所有文本及其位置 # 这里需要将图像预处理为灰度、二值化等以提高OCR精度,为简化省略 data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT) # 3. 根据element_key推断可能的标签文本(这里需要维护一个映射表,简化处理) label_map = { “login_page.username_input”: [“用户名”, “账号”, “手机号”, “邮箱”, “User”, “Username”], “login_page.password_input”: [“密码”, “Password”, “密碼”], } target_labels = label_map.get(element_key, []) # 4. 在OCR结果中搜索目标标签 for i, text in enumerate(data[‘text’]): if text.strip() in target_labels: # 找到标签,假设输入框在标签右侧或下方一定区域内 x, y, w, h = data[‘left’][i], data[‘top’][i], data[‘width’][i], data[‘height’][i] # 计算输入框可能区域(例如,标签右侧50像素开始,宽度200像素) input_box_x, input_box_y = x + w + 50, y # 这里可以进一步用图像识别确认该区域是输入框,或直接返回一个Playwright坐标选择器 # 返回一个基于坐标的定位器(近似,不精确) # 更优解:用Playwright的定位函数结合OCR识别的文本附近查找input元素 # 示例:尝试在标签元素后面找input # 这里仅示意返回一个通过文本附近查找的Playwright选择器 # 实际中,可以尝试多种策略组合 return self.page.locator(f“input:near(:text(‘{text}’))”) # Playwright 1.32+ 支持:near print(f“AI Fallback: Could not locate label for {element_key} via OCR.”) return None def locate_button_by_text(self, element_key: str): """通过OCR识别按钮文本来定位按钮。即使按钮的CSS类或ID变了,只要文本没变就能找到。""" screenshot_path = f“temp_screenshot_{int(time.time())}.png” self.page.screenshot(path=screenshot_path) img = cv2.imread(screenshot_path) data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT, lang=‘chi_sim+eng’) # 中英文 button_text_map = {“login_page.submit_button”: [“登录”, “登陆”, “Sign In”, “Submit”]} target_texts = button_text_map.get(element_key, []) for i, text in enumerate(data[‘text’]): if text.strip() in target_texts: x, y = data[‘left’][i], data[‘top’][i] # 返回一个基于文本的Playwright定位器,这是最可靠的,因为Playwright内部也有文本查询引擎 # OCR用于确认文本在屏幕上,但最终定位交给Playwright更稳定 return self.page.get_by_text(text.strip(), exact=True) # Playwright推荐的方式 return None实操心得:纯视觉AI定位(OCR+CV)在复杂UI、动态内容、字体渲染差异下准确率有限,且执行速度较慢。它最适合作为主定位器失效后的“最后一搏”,或者用于验证某些无法通过DOM属性定位的元素(如Canvas绘制的按钮)。在生产环境中,更常见的“Midscene”实践是强化映射层的数据驱动能力,比如将定位器信息存储在外部数据库,并提供友好的管理界面,让非技术人员也能在UI变更后更新定位器。
3.4 整合三层:编写页面对象与测试用例
现在,我们将三层架构整合起来。首先在conftest.py中初始化Playwright和注册表。
# conftest.py import pytest from playwright.sync_api import Page from midscene.locator_registry import LocatorRegistry @pytest.fixture(scope=“function”) def page_with_registry(browser): # 创建浏览器上下文和页面 context = browser.new_context() page = context.new_page() # 为页面注入定位器注册表 registry = LocatorRegistry(page) # 可以将registry挂载到page对象上,方便使用 page.locator_registry = registry yield page context.close() @pytest.fixture(scope=“session”) def browser(playwright): # 启动Chromium,可配置为headed模式调试 browser = playwright.chromium.launch(headless=False, slow_mo=500) # 调试时可关闭无头模式并放慢速度 yield browser browser.close()接着,创建页面对象,它代表“业务场景层”的一部分,封装了页面上的操作。
# pages/login_page.py class LoginPage: def __init__(self, page): self.page = page self.registry = page.locator_registry # 使用注入的注册表 def navigate(self, url): self.page.goto(url) def enter_username(self, username: str): # 使用业务键,而非具体选择器 locator = self.registry.get_locator(“login_page.username_input”) locator.click() locator.fill(username) def enter_password(self, password: str): locator = self.registry.get_locator(“login_page.password_input”) locator.fill(password) def click_submit(self): locator = self.registry.get_locator(“login_page.submit_button”) locator.click()最后,编写一个真正的测试用例,它只关注业务流。
# tests/test_login.py def test_admin_login_successful(page_with_registry): """ 测试管理员成功登录。 业务场景层:只描述“做什么”,不关心“怎么做”。 """ login_page = LoginPage(page_with_registry) # 1. 导航到登录页 login_page.navigate(“https://your-test-app.com/login”) # 2. 输入凭据 login_page.enter_username(“admin”) login_page.enter_password(“securepassword123”) # 3. 提交登录 login_page.click_submit() # 4. 断言登录成功(同样使用注册表定位欢迎信息) welcome_locator = page_with_registry.locator_registry.get_locator(“dashboard.welcome_text”) # 等待元素出现,增加稳定性 welcome_locator.wait_for(state=“visible”) assert “欢迎回来,管理员” in welcome_locator.inner_text()这个测试用例非常清晰。如果某天登录页重构,按钮的HTML从<button>登录</button>变成了<div class=“btn-primary”>Sign In</div>,我们只需要去更新locator_registry.py中“login_page.submit_button”的“primary”策略为“div.btn-primary:has-text(‘Sign In’)”。所有用到这个按钮的测试用例都无需修改。这就是“Midscene”带来的维护性红利。
4. 进阶:动态映射、自愈与CI/CD集成
基础的“Midscene”框架搭建好后,我们可以考虑一些进阶特性,让它更强大、更智能。
4.1 实现动态映射管理与自愈
静态的Python字典注册表不利于维护。我们可以将其外置为JSON或YAML文件,甚至存入数据库。
# locators.yaml login_page: username_input: primary: “#username” fallback: “locate_by_placeholder_and_label” description: “用户名输入框” version: “1.2.0” # 关联应用版本 submit_button: primary: “button:has-text(‘登录’)” fallback: “locate_button_by_text”框架启动时加载这个文件。更进一步,可以开发一个简单的管理界面,允许手动更新定位器,或者在AI备用策略成功定位后,提示用户是否将新的定位策略更新到映射库中,实现“半自动自愈”。
自愈流程可以这样设计:
- 主定位器失败,触发AI备用策略。
- AI策略成功定位到元素。
- 框架记录下AI成功使用的定位信息(例如,AI最终是通过
get_by_text(‘登录’)找到的)。 - 在测试运行结束后,生成报告,建议将
“login_page.submit_button”的主定位器更新为“button:has-text(‘登录’)”。 - 经过人工审核或自动规则验证后,更新映射文件。
4.2 与CI/CD管道集成
一个健壮的自动化测试框架必须能无缝融入CI/CD。基于“Midscene”的框架在这方面有天然优势,因为它的稳定性更高。
# 示例:GitLab CI 配置 (.gitlab-ci.yml) stages: - test ui-automation: stage: test image: mcr.microsoft.com/playwright/python:v1.40.0-focal before_script: - apt-get update && apt-get install -y tesseract-ocr tesseract-ocr-chi-sim # 安装OCR依赖 - pip install -r requirements.txt - playwright install --with-deps script: - python -m pytest tests/ --alluredir=./allure-results # 运行测试并生成Allure报告 artifacts: when: always paths: - ./allure-results/ - ./test-screenshots/ # 保存失败截图 expire_in: 1 week after_script: # 可选:分析本次运行结果,如果发现大量定位失败,可以触发警报或自动尝试更新定位器建议。在CI中运行时,务必配置好无头模式,并妥善管理浏览器依赖。测试报告(如Allure)可以清晰地展示哪些用例因定位问题失败,帮助快速定位UI变更的影响范围。
4.3 应对复杂场景:Shadow DOM、Canvas与跨平台
热词中提到了Avalonia UI、Vue、Angular等,这些现代框架常涉及Shadow DOM或复杂的组件结构。Playwright本身对Shadow DOM有很好的支持,可以通过>>>或/deep/选择器穿透。在“Midscene”映射层,我们可以将这些复杂的选择器封装起来。
对于Canvas、游戏UI等无法通过DOM树定位的场景,AI视觉定位(图像模板匹配、特征点识别)就成了主要甚至唯一手段。这时,“Midscene”映射层存储的就不是CSS选择器,而是参考图像的路径或特征模型。例如:
self.registry[“game.start_button”] = { “primary”: “locate_by_image_template”, “params”: {“template_path”: “./templates/start_button.png”, “threshold”: 0.9} }对于“2026年跨平台自动化测试工具”的愿景,“Midscene”架构同样适用。映射层可以存储多套定位策略,针对Android、iOS、Web等不同平台。业务场景层的测试逻辑保持不变,执行时根据当前测试平台选择对应的定位策略即可。
5. 常见问题、排查技巧与避坑指南
在实际搭建和运行“Midscene”框架时,你会遇到各种问题。以下是我从实战中总结的一些典型问题和解决方案。
5.1 定位器失效问题排查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 主定位器找不到元素 | 1. 前端代码已变更,属性/结构已不同。 2. 页面未加载完成或处于动态加载状态。 3. 元素在iframe或Shadow DOM内。 | 1.检查映射:首先确认locator_registry中的主定位器是否已更新为最新前端的有效选择器。使用浏览器开发者工具验证。2.增加等待:在操作前使用 locator.wait_for(state=“visible”)或page.wait_for_selector()。3.检查上下文:确认是否需要在iframe或Shadow DOM内查找。Playwright提供 frame.locator()和shadow_root属性。 |
| AI备用策略误识别或失败 | 1. 截图质量差、光线/分辨率变化。 2. OCR语言包未安装或识别率低。 3. 模板匹配的图片区域有动态内容。 | 1.优化截图:确保截图清晰,可尝试在操作前等待动画完成(page.wait_for_timeout)。2.配置OCR:安装正确的Tesseract语言包(如 chi_sim),对截图进行预处理(灰度化、二值化、降噪)。3.优化模板:使用更具唯一性的图像区域作为模板,或考虑使用特征匹配(SIFT/SURF)而非绝对模板匹配。 |
| 测试执行速度显著变慢 | 1. AI备用策略被频繁触发(说明主定位器大量失效)。 2. 截图和图像处理耗时。 | 1.首要任务:修复主定位器。AI备用是保险丝,不是常态。频繁触发说明映射层维护不及时。 2.性能优化:仅在主定位器失败时触发AI。对AI函数进行性能剖析,缓存静态模板,考虑使用更快的图像库(如 opencv-python-headless)。 |
| 同一业务键在不同页面/状态下匹配错误 | 映射键定义过于宽泛,缺乏上下文。 | 细化映射键:将“submit_button”细化为“login.submit_button”和“order.submit_button”。或者在注册表中增加上下文条件(如“when_page_url_contains”: “/login”)。 |
5.2 框架维护的心得与技巧
映射库是资产,不是代码:一定要将定位器映射与测试脚本分离存储(如YAML/JSON/DB)。最好能提供一个Web管理界面,让产品经理或UI设计师在修改设计稿后,能方便地提交定位器变更请求,甚至通过拖拽生成新的图像模板。
AI定位是辅助,逻辑定位是根本:不要过度依赖视觉AI。只要有可能,优先使用基于语义的、稳定的逻辑定位器,如
get_by_role()、get_by_text()、get_by_test_id()。与开发团队约定使用>