AI 驱动 Selenium 测试框架最佳实践:从传统自动化到智能体测试

一、引言:为什么传统 Selenium 框架正在被重新定义
在自动化测试领域摸爬滚打十年后,我越来越清晰地意识到:传统 Selenium 框架正站在一个转折点上。
过去十年,我们习惯了这样的工作流:
- 手动审查页面 DOM
- 精心编写 CSS Selector 或 XPath
- 祈祷前端团队不要改 class 名
- 每次前端重构都花两天修定位器
- 维护一个越来越庞大的元素仓库,没人敢动
这不是自动化,这是维护负债的永续债。
2024--2025 年,大语言模型(LLM)的能力跨越了一个临界点------AI 不再是仅仅能写代码的辅助工具,而是可以实时理解页面结构、推理元素位置、并在元素变化时自动修复定位器的智能引擎。
我们需要一个 生产级 AI Selenium 测试框架,它不是一个概念验证,而是一个在真实项目中运行、支持 Claude / OpenAI / 本地模型三种 AI 提供商、具备自然语言驱动测试生成能力的完整框架。
本文将分享我在这个过程中的全部技术决策、架构模式和踩坑教训。
二、传统 Selenium 自动化的四大痛点
在进入 AI 方案之前,先对齐问题域。任何不谈痛点的方案都是耍流氓。
痛点 1:定位器的脆弱性
python
# 传统写法 ------ 前端改了 card-body__wrapper,你就死了
driver.find_element(By.CSS_SELECTOR, ".card-body__wrapper .btn-primary")
前端框架的普及(React / Vue / Angular)带来了动态 class 名、组件化 DOM 结构,让传统定位器频繁失效。CSS class 的语义从"描述元素是什么"退化为"描述元素长什么样",一夜之间就能变。
痛点 2:维护成本随项目规模超线性增长
一个 200 个测试用例的项目,如果页面元素变更 20%:
- 每个用例平均修复时间:15 分钟
- 批量修复时间:200 × 15 = 3000 分钟 = 50 小时人天
- 且修复后的定位器在下一次改版可能再次失效
痛点 3:自然语言需求到自动化脚本的翻译鸿沟
产品经理说:"在搜索框输入关键词,点搜索,验证结果页包含相关内容。"
测试人员要做的是:
- 理解这个描述
- 手动录制 / 编写操作步骤
- 手动定位页面元素
- 编写断言
- 处理等待和异常
五步里只有第一步是思维活动,剩下四步都是机械劳动。
痛点 4:页面元素变化时的"瞎子"模式
传统 Selenium 的 find_element 是一个刚性调用 :找到了就继续,找不到就抛出 NoSuchElementException 结束测试。它没有"换个方式找找"的能力,更没有"记住新位置,下次直接去"的智能。
三、AI 驱动的架构设计:核心原则
基于上述痛点,我在设计 AI Selenium 框架时确立了以下核心原则:
原则 1:渐进增强(Progressive Enhancement)
传统方式 100% 兼容,AI 作为能力增强层,而非替代层。
框架不做"推翻重来"的事。你之前写的所有 Selenium 代码------所有 find_element、所有 Page Object------都能继续跑。AI 在失败时介入,而不是在正常时抢道。
原则 2:语义优先,定位器次之
每个元素都应该有一个"人类能理解的语义描述",定位器只是实现细节。
python
# 传统 Page Object
class LoginPage:
def __init__(self, driver):
self.login_btn = driver.find_element(By.ID, "login-btn")
# AI 增强的 Page Object
class LoginPage(BasePage):
@property
def login_btn(self):
return self.element("登录按钮", locator=("id", "login-btn"))
当 id="login-btn" 失效时,AI 根据"登录按钮"这个语义描述 + 当前页面 HTML 结构,实时推断出新的定位器 。传统的 login_btn 属性变成了两阶段策略:优先用定位器,失败后用 AI。
原则 3:优雅降级(Graceful Degradation)
没有 AI API Key 时,框架应该仍然可用。
这是很多 AI 框架容易忽视的一点。我们实现了三层降级:
AI 在线模式 → AI 离线 Mock 模式 → 纯传统 Selenium 模式
每一层都向下兼容,确保开发环境、CI/CD 和企业内网环境都能运行。
原则 4:自愈能力应该是透明的
测试人员不需要知道元素被"修复"了------框架默默地做这件事,记录下来,下次更快。
四、AI 元素定位:框架的核心技术突破
4.1 两阶段定位器解析
这是框架最核心的创新能力。每个 AIElement 在第一次访问时执行以下流程:
#mermaid-svg-PhExjB39tNUfSKkn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PhExjB39tNUfSKkn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PhExjB39tNUfSKkn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PhExjB39tNUfSKkn .error-icon{fill:#552222;}#mermaid-svg-PhExjB39tNUfSKkn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PhExjB39tNUfSKkn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PhExjB39tNUfSKkn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PhExjB39tNUfSKkn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PhExjB39tNUfSKkn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PhExjB39tNUfSKkn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PhExjB39tNUfSKkn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PhExjB39tNUfSKkn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PhExjB39tNUfSKkn .marker.cross{stroke:#333333;}#mermaid-svg-PhExjB39tNUfSKkn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PhExjB39tNUfSKkn p{margin:0;}#mermaid-svg-PhExjB39tNUfSKkn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PhExjB39tNUfSKkn .cluster-label text{fill:#333;}#mermaid-svg-PhExjB39tNUfSKkn .cluster-label span{color:#333;}#mermaid-svg-PhExjB39tNUfSKkn .cluster-label span p{background-color:transparent;}#mermaid-svg-PhExjB39tNUfSKkn .label text,#mermaid-svg-PhExjB39tNUfSKkn span{fill:#333;color:#333;}#mermaid-svg-PhExjB39tNUfSKkn .node rect,#mermaid-svg-PhExjB39tNUfSKkn .node circle,#mermaid-svg-PhExjB39tNUfSKkn .node ellipse,#mermaid-svg-PhExjB39tNUfSKkn .node polygon,#mermaid-svg-PhExjB39tNUfSKkn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PhExjB39tNUfSKkn .rough-node .label text,#mermaid-svg-PhExjB39tNUfSKkn .node .label text,#mermaid-svg-PhExjB39tNUfSKkn .image-shape .label,#mermaid-svg-PhExjB39tNUfSKkn .icon-shape .label{text-anchor:middle;}#mermaid-svg-PhExjB39tNUfSKkn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PhExjB39tNUfSKkn .rough-node .label,#mermaid-svg-PhExjB39tNUfSKkn .node .label,#mermaid-svg-PhExjB39tNUfSKkn .image-shape .label,#mermaid-svg-PhExjB39tNUfSKkn .icon-shape .label{text-align:center;}#mermaid-svg-PhExjB39tNUfSKkn .node.clickable{cursor:pointer;}#mermaid-svg-PhExjB39tNUfSKkn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PhExjB39tNUfSKkn .arrowheadPath{fill:#333333;}#mermaid-svg-PhExjB39tNUfSKkn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PhExjB39tNUfSKkn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PhExjB39tNUfSKkn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PhExjB39tNUfSKkn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PhExjB39tNUfSKkn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PhExjB39tNUfSKkn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PhExjB39tNUfSKkn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PhExjB39tNUfSKkn .cluster text{fill:#333;}#mermaid-svg-PhExjB39tNUfSKkn .cluster span{color:#333;}#mermaid-svg-PhExjB39tNUfSKkn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PhExjB39tNUfSKkn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PhExjB39tNUfSKkn rect.text{fill:none;stroke-width:0;}#mermaid-svg-PhExjB39tNUfSKkn .icon-shape,#mermaid-svg-PhExjB39tNUfSKkn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PhExjB39tNUfSKkn .icon-shape p,#mermaid-svg-PhExjB39tNUfSKkn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PhExjB39tNUfSKkn .icon-shape .label rect,#mermaid-svg-PhExjB39tNUfSKkn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PhExjB39tNUfSKkn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PhExjB39tNUfSKkn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PhExjB39tNUfSKkn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🤖 AI 定位流程
是
是
是
否
否
否
🖱️ 用户请求元素
是否有显式定位器
CSS / XPath / ID
尝试定位
成功
缓存 WebElement
返回
1️⃣ 获取当前页面 HTML
清理后
2️⃣ 构建 Prompt → LLM
3️⃣ LLM 返回定位结果
confidence ≥ 阈值
默认 0.85
缓存定位器
写入元素仓库
持久化
4️⃣ 使用新定位器
重试 find_element
记录到 healing_log
人工审核
4.2 Prompt 设计:如何让 AI 准确输出定位器
这里的关键不是"把 html 全扔给 AI",而是结构性表达。我们的 Prompt 模板核心结构如下:
你是一个 Selenium 元素定位专家。给定页面的 HTML 结构和元素描述,
返回该元素最可能的定位策略。
HTML 结构(已清理 script/style/注释):
{cleaned_html}
元素描述:{element_description}
请返回 JSON 格式:
{{
"locator_type": "css_selector | xpath | id | name | class_name | text",
"locator_value": "...",
"confidence": 0.0-1.0,
"reasoning": "简要推理过程",
"fallback_strategies": ["备选策略1", "备选策略2"]
}}
关键设计决策:
- 清理 HTML :移除
<script>、<style>、注释、空属性,保留id、class、data-*、name、type、placeholder、aria-*、text()等有用属性。这既节省 token 又提高准确率。 - 要求 confidence 评分:让 AI 自己评估可靠程度,低于阈值时触发人工审核流程。
- fallback_strategies:如果主策略失败,框架可以尝试备选方案,不需要再次调用 LLM。
- reasoning 字段:记录 AI 的推理过程,方便调试和审查。
4.3 缓存策略:避免重复调用 LLM
LLM API 调用有三大问题:延迟高、成本高、有失败风险。我们设计了三级缓存:
| 缓存层级 | 作用域 | 失效条件 | 命中时延迟 |
|---|---|---|---|
| 内存缓存(dict) | 当前测试会话 | 页面 URL 变化 | ~0ms |
| 元素仓库(YAML) | 持久化跨会话 | 手动清理 | ~5ms |
| 会话内 WebElement 缓存 | 单个元素实例 | 元素被刷新 | ~0ms |
python
def _ai_locate(self):
"""AI 定位------带三级缓存"""
cache_key = f"{self.description}|{self.driver.current_url}"
# 1. 内存缓存
if cache_key in self._locator_cache:
return self._locator_cache[cache_key]
# 2. 元素仓库缓存(YAML)
cached = self._repo_lookup(self.description)
if cached:
self._locator_cache[cache_key] = cached
return cached
# 3. LLM 调用
result = self._llm_locate_element()
self._locator_cache[cache_key] = result
self._repo_persist(self.description, result)
return result
实际效果 :一个测试套件中,LLM 调用通常只在首次运行 或页面改版后发生。日常运行几乎零 AI 开销。
4.4 多 AI 提供商抽象层
我们不绑定任何特定 AI 提供商。通过统一的 LLMClient 抽象层,支持以下后端:
python
class LLMClient:
async def ask(self, prompt: str) -> str:
if provider == "anthropic":
return await self._ask_claude(prompt)
elif provider == "openai":
return await self._ask_openai(prompt)
elif provider == "local":
return await self._ask_local(prompt)
else:
return self._mock_response(prompt) # 无 API Key 时降级
| 提供商 | 默认模型 | 推荐场景 |
|---|---|---|
| Claude (Anthropic) | claude-sonnet-4-20250514 | 元素定位准确率最高,推理能力强 |
| OpenAI | gpt-4o | 兼容性好,生态成熟 |
| 本地 (Ollama) | qwen2.5:7b | 内网部署,无数据离开风险 |
实测经验 :在元素定位任务上,Claude Sonnet 的准确率最高,尤其是在需要理解"属性值含义"(如
aria-label="搜索"与button文本的语义匹配)时表现突出。
五、自愈引擎:让测试脚本自我修复
5.1 透明代理模式
这是整个框架中我最自豪的设计------通过 monkey-patch 实现全局透明自愈:
python
# conftest.py
@pytest.fixture(autouse=True)
def auto_heal(driver, heal_engine):
original_find = driver.find_element
def healing_find(by, value):
for attempt in range(3): # 最多重试 3 次
try:
return original_find(by, value)
except NoSuchElementException:
# 使用 AI 修复定位器
new_by, new_value = heal_engine.heal(by, value)
by, value = new_by, new_value
continue # 用新定位器重试
raise NoSuchElementException(f...)
driver.find_element = healing_find
yield
driver.find_element = original_find # 恢复原始方法
为什么这是最佳方案?
- 零侵入:被测代码不需要 import 任何自愈相关模块
- 全局生效 :所有
find_element调用都自动获得自愈能力,包括第三方库 - 透明:测试用例完全不知道自愈发生了
- 可控 :通过
--ai-heal/--no-ai-healCLI 标志在运行时可开关
5.2 自愈流程详解
driver.find_element(By.ID, "login-btn")
│
└── 抛出 NoSuchElementException
│
├── 1. HealEngine 在元素仓库中查找 "login-btn" 的语义描述
├── 2. 使用 AIElementLocator 根据语义描述重新定位
├── 3. AI 返回新定位器(confidence: 0.92)
├── 4. confidence ≥ 0.85 → 自动写回元素仓库
├── 5. 记录 healing_log.jsonl
├── 6. 使用新定位器重试 → 成功
└── 7. 测试继续执行,测试人员不需要知道发生了改变
5.3 置信度阈值策略
| 阈值区间 | 处理方式 |
|---|---|
| ≥ 0.95 | 自动批准 + 写入仓库 + 静默重试 |
| 0.85 -- 0.94 | 自动批准 + 写入仓库 + 打 @pytest.mark.healed 标记 |
| 0.70 -- 0.84 | 写入仓库标 pending_review + 记录日志 |
| < 0.70 | 仅记录日志 + 继续使用传统尝试 |
这个阈值体系是我在实践中反复调优得到的。太保守(> 0.95)会导致大量需要人工介入;太激进(< 0.80)会把错误定位器写进仓库,造成连锁失败。
5.4 Healing Log:可审计的自愈轨迹
每次自愈事件都会记录到 element_repository/healing_log.jsonl:
json
{
"timestamp": "2025-06-10T14:32:18.123Z",
"test": "test_login",
"element": "登录按钮",
"old_locator": {"type": "id", "value": "login-btn"},
"new_locator": {"type": "css_selector", "value": "button[data-testid='login-submit']"},
"confidence": 0.92,
"reasoning": "原login-btn未在页面中找到,发现button[data-testid='login-submit']具有相同的文本'登录'和功能语义",
"page_url": "https://example.com/login"
}
这个日志在项目中的实际价值:
- 当自愈发生异常时,可以回溯排查
- 为前端团队提供"你们的改动影响了哪些元素"的数据反馈
- 评估 AI 定位准确率的原始数据源
六、自然语言驱动的测试生成:从一句话到可执行测试
这是框架面向"让非技术人员也能写自动化测试"的尝试。用户只需要用自然语言描述操作步骤,框架自动生成 Page Object + 测试代码。
6.1 支持的 NLP 指令模式
框架内置了一个结构化自然语言解析器,支持以下指令模式:
| 你说的话 | 框架生成的动作 | 示例 |
|---|---|---|
| "打开 {url}" | driver.get(url) / page.open(url) |
"打开 https://baidu.com" |
| "在 {元素} 输入 {文本}" | 定位元素 → send_keys | "在搜索框输入 AI测试" |
| "点击 {元素}" | 定位元素 → click | "点击登录按钮" |
| "等待 N 秒" | time.sleep(N) |
"等待3秒" |
| "截图" | 截屏保存 | "截图保存" |
| "验证 {元素} 包含 {文本}" | 断言文本包含 | "验证结果包含成功" |
| "标题包含 {文本}" | 断言 title | "标题包含首页" |
| "滚动到 {元素}" | scrollIntoView | "滚动到底部" |
| "悬停到 {元素}" | ActionChains.move_to_element | "悬停到菜单" |
6.2 生成管线
pytest 文件系统 代码生成器 PO推断器 LLM解析器 pytest 文件系统 代码生成器 PO推断器 LLM解析器 #mermaid-svg-PI7xzbYhRfxNHol1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PI7xzbYhRfxNHol1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PI7xzbYhRfxNHol1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PI7xzbYhRfxNHol1 .error-icon{fill:#552222;}#mermaid-svg-PI7xzbYhRfxNHol1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PI7xzbYhRfxNHol1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PI7xzbYhRfxNHol1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PI7xzbYhRfxNHol1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PI7xzbYhRfxNHol1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PI7xzbYhRfxNHol1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PI7xzbYhRfxNHol1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PI7xzbYhRfxNHol1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PI7xzbYhRfxNHol1 .marker.cross{stroke:#333333;}#mermaid-svg-PI7xzbYhRfxNHol1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PI7xzbYhRfxNHol1 p{margin:0;}#mermaid-svg-PI7xzbYhRfxNHol1 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PI7xzbYhRfxNHol1 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-PI7xzbYhRfxNHol1 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-PI7xzbYhRfxNHol1 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-PI7xzbYhRfxNHol1 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-PI7xzbYhRfxNHol1 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-PI7xzbYhRfxNHol1 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-PI7xzbYhRfxNHol1 .sequenceNumber{fill:white;}#mermaid-svg-PI7xzbYhRfxNHol1 #sequencenumber{fill:#333;}#mermaid-svg-PI7xzbYhRfxNHol1 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-PI7xzbYhRfxNHol1 .messageText{fill:#333;stroke:none;}#mermaid-svg-PI7xzbYhRfxNHol1 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PI7xzbYhRfxNHol1 .labelText,#mermaid-svg-PI7xzbYhRfxNHol1 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-PI7xzbYhRfxNHol1 .loopText,#mermaid-svg-PI7xzbYhRfxNHol1 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-PI7xzbYhRfxNHol1 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-PI7xzbYhRfxNHol1 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-PI7xzbYhRfxNHol1 .noteText,#mermaid-svg-PI7xzbYhRfxNHol1 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-PI7xzbYhRfxNHol1 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PI7xzbYhRfxNHol1 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PI7xzbYhRfxNHol1 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-PI7xzbYhRfxNHol1 .actorPopupMenu{position:absolute;}#mermaid-svg-PI7xzbYhRfxNHol1 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-PI7xzbYhRfxNHol1 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-PI7xzbYhRfxNHol1 .actor-man circle,#mermaid-svg-PI7xzbYhRfxNHol1 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-PI7xzbYhRfxNHol1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输出 listTestStep {action, url/element, text} 从 URL 提取域名 → 推断 "BaiduPage" alt 自动运行(可选) 仅生成 用户 自然语言描述 listTestStep PO名称 + TestStep 生成 pages/ai_generated/baidu_page.py 组合测试用例 生成 tests/ai_generated/test_baidu.py 调用 pytest 测试结果 返回文件路径 用户
6.3 生成的代码品质
这部分的挑战不在于"能否生成",而在于生成可维护的代码。以下是一个真实生成示例(整理后):
python
# pages/ai_generated/baidu_page.py
from pages.base_page import BasePage
class BaiduPage(BasePage):
url = "https://www.baidu.com"
@property
def 搜索框(self):
return self.element("百度首页搜索输入框")
@property
def 百度一下(self):
return self.element("百度一下搜索按钮")
def search(self, keyword: str):
self.open()
self.搜索框.input(keyword)
self.百度一下.click()
return self
python
# tests/ai_generated/test_baidu.py
import pytest
from pages.ai_generated.baidu_page import BaiduPage
@pytest.mark.ai_generated
class TestBaidu:
def test_baidu_search(self, driver):
page = BaiduPage(driver)
page.search("AI Selenium testing")
assert "AI" in page.title
生成的代码有两个关键设计:
- AIElement 无显式定位器:所有元素依赖 AI 运行时定位。这意味着即使页面变化,生成的代码也不需要修改。
- 语义化的属性名 :直接使用中文(如
self.搜索框),降低理解门槛。
七、多浏览器驱动工厂与配置体系
7.1 DriverFactory 设计
DriverFactory.create_driver(browser="chrome", headless=True)
│
├── browser="chrome"
│ ├── webdriver-manager 自动下载 ChromeDriver
│ ├── 反检测配置(隐藏自动化标识)
│ └── 性能优化(禁用无用特性)
│
├── browser="firefox"
│ └── GeckoDriver + Firefox Options
│
├── browser="edge"
│ └── EdgeDriver + Edge Options
│
└── remote_url="http://grid:4444/wd/hub"
└── webdriver.Remote 连接到 Selenium Grid
反检测配置示例(对爬虫类任务有用,测试环境也可避免反爬干扰):
python
if browser == "chrome":
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
7.2 三层配置体系
| 层级 | 来源 | 配置项 | 优先级 |
|---|---|---|---|
| 1. CLI 参数 | --browser chrome --headless --ai-provider anthropic |
运行时切换 | 最高 |
| 2. 环境变量 + .env | ANTHROPIC_API_KEY, AI_PROVIDER |
API 密钥、环境配置 | 中 |
| 3. YAML 配置文件 | config/config.yaml |
默认参数、阈值、路径 | 低 |
推荐项目实践:
- YAML 配置提交到 git,作为团队默认值
.env不提交到 git (已在.gitignore中),每个人有自己的 API Key- CLI 参数用于 CI/CD 的特定配置(如
--headless)
八、验证码处理策略(一个务实方案)
验证码是自动化测试的"最后一公里"。我们的方案采用了多策略降级:
python
class CaptchaSolver:
def solve(self, image_path: str) -> str:
# 策略 1: 环境变量指定(CI 环境常用万能验证码)
if captcha_code := os.getenv("CAPTCHA_CODE"):
return captcha_code
# 策略 2: Tesseract OCR(简单验证码)
for preprocessor in [grayscale, threshold, denoise]:
result = ocr_with_preprocess(image_path, preprocessor)
if self._is_valid(result):
return result
# 策略 3: 人工介入(保存图片,等待用户输入)
return self._manual_input(image_path)
实际项目建议:
- 单元测试 / 集成测试 :通过 API 绕过验证码(如
login_via_api()直接设置 token) - E2E 测试:使用测试环境的万能验证码
- OCR 方案:仅用于非核心流程的兜底
九、最佳实践总结(来自真实项目的经验)
9.1 架构层面
| 实践 | 详细说明 |
|---|---|
| Page Object 是基础,AI 是增强层 | AI 不能替代良好的架构设计,它让好架构变得更好 |
| AI 定位作为第二路径,不是唯一路径 | 有显式定位器时优先使用,能避免 95% 的 AI 调用 |
| 不做全量 AI 替代 | 只在异常时触发 AI,而非每个元素都走 AI 定位 |
| 一定要有 Mock 模式 | 没有 API Key 时框架必须可用,这是 CI 不可达环境的基础 |
9.2 Prompt 工程层面
| 实践 | 详细说明 |
|---|---|
| 发送前清洗 HTML | 移除 script/style/注释/空属性,可减少 60-80% 的 token 消耗 |
| 要求 confidence 评分 | 让 LLM 自评估,低分结果不自动写入仓库 |
| 结构化输出(JSON) | 比自由文本更易解析,再用 Pydantic/Schema 验证 |
| 提供 fallback 策略 | AI 返回时附带备选定位方案,减少二次调用 |
| 使用英文 Prompt | 实测英文 Prompt 的定位准确率高于中文 5-10% |
9.3 维护层面
| 实践 | 详细说明 |
|---|---|
| 元素仓库提交到 git | 追踪定位器变更历史,方便回滚错误修复 |
| Healing Log 定期审查 | 每周检查自愈日志,定位器频繁变化的页面需要通知前端团队 |
| AI 生成测试纳入 Code Review | AI 生成的测试代码需要人工审核后再加入回归套件 |
| 定期清理 Mock 数据 | 避免 Mock 模式下积累不可靠的测试数据 |
9.4 性能优化
| 实践 | 说明 |
|---|---|
| AI 定位结果会话内缓存 | 同一个测试会话内对同一元素的重复请求直接从内存返回 |
| 异步 LLM 调用 | 使用 asyncio 避免阻塞测试线程 |
| 控制单次发送的 HTML 大小 | 过大的页面只发送可见区域或关键容器 |
| CI 中使用 --no-ai-heal | 生产 CI 中 AI 修复是风险(因为无人工审核),只在开发/预发布环境开启自愈 |
十、框架选型建议:什么时候该用 AI Selenium?
适合 AI 增强的场景
| 场景 | 推荐度 | 理由 |
|---|---|---|
| 前端频繁改版(React/Vue 项目) | ⭐⭐⭐⭐⭐ | 自愈能力直接降低维护成本 |
| 多浏览器兼容测试 | ⭐⭐⭐⭐ | AI 理解各浏览器渲染差异,自动适配 |
| 快速原型验证 | ⭐⭐⭐⭐⭐ | 自然语言 → 测试脚本,分钟级产出 |
| 长流程 E2E 测试 | ⭐⭐⭐⭐ | 中间页面的变化不影响后续步骤 |
| 新项目初期测试建设 | ⭐⭐⭐⭐⭐ | 无需精确定位器,语义描述即可 |
不适合的场景
| 场景 | 理由 |
|---|---|
| 静态页面,从不变化 | 传统定位器足够,AI 是过度设计 |
| 高频率定时运行(如每分钟) | AI 调用的延迟和成本不值得 |
| 严格离线环境,无本地模型 | 无法运行 LLM |
| 页面极度复杂(数千元素) | HTML 太大导致 token 成本过高 |
十一、未来方向:从自愈到自主测试
基于当前框架的实践,我认为 AI Selenium 的下一个演进方向是:
- 视觉定位层:当 DOM 定位器全部失败时,使用截图 + 视觉 AI 定位(如 Claude Vision),实现真正的"所见即所得"
- AI 驱动的断言:不硬编码预期结果,而是让 AI 理解业务规则并自动验证
- 失败根因分析:测试失败时,AI 自动分析日志 + 截图 + DOM,定位是元素变化还是业务逻辑问题
- 测试生成自优化:根据历史运行数据,AI 自动调整等待时间、定位策略、重试逻辑
这些方向我们在下一个版本已经开始探索------核心思路是让框架越来越不需要人工介入,但不是通过硬编码规则,而是通过 AI 的推理能力。
十二、写在最后
构建这个 AI Selenium 框架的过程中,我最大的体会是:
AI 不是在"替代"测试工程师,而是在"解放"测试工程师。
过去我们需要花费 60% 的时间在定位器维护和脚本调试上,只有 40% 的时间在做真正的测试设计。AI 框架的目标是把这个比例倒过来------让 AI 处理那些重复、脆弱、机械的工作,让人专注于不可替代的部分:测试策略、场景设计、质量风险评估。
附:本文框架实践地址 ai-selenium-framework,一个生产级的 AI 驱动 Selenium 测试框架,支持 Claude / OpenAI / 本地模型多提供商,具备自愈定位、自然语言测试生成、自动截图报告等完整能力集,代码已开源。