用 AI 做执行自动化 ·(一)
上一个子系列我们走完了测试设计------脑暴、等价类边界、场景判定表,最后整合成一条条八字段用例。但用例写得再漂亮,不落到能跑、能回归的脚本里,价值只兑现了一半。
我猜很多人第一次让 AI 写自动化脚本,体验是这样的:把用例丢过去,它刷刷刷给你一段代码,复制到本地------咦,能跑,绿了。可等你想把它放进项目、接进 CI、跟别人的脚本一起回归,问题全来了:它自己 webdriver.Chrome() 起了个浏览器、选择器是一串长 XPath、到处 time.sleep(3)、数据写死、还没断言。
这不是"能跑的脚本",这是一段"恰好这次跑通了的 demo"。
这一篇我想讲透一件事:怎么让 AI 在你"现有的框架"里写脚本,写出来就能直接合入。 老规矩,先讲原理,再看清要躲哪些坑,最后给可直接照抄的方法。
一、原理:AI 写脚本时,到底在干什么
1.1 它不是"翻译用例",而是在"预测你项目里的代码长什么样"
很多人以为:我给它用例,它照着步骤翻译成代码就行了。但大模型生成代码的真实机制是基于上下文的模式补全------它看你给的信息,去预测"在这样一个项目里,工程师这段代码通常怎么写"。
这里有个关键分叉:
- 你给了它项目上下文(技术栈、目录结构、已有的 BasePage、POM、fixture)→ 它就模仿这些已有代码的写法,产出"项目风格"的脚本;
- 你只给了用例、什么上下文都没给 → 它手里没有任何可参照的"本项目模式",只能回退到训练数据里最通用的写法 :独立的 Selenium demo、绝对 XPath、
time.sleep、硬编码、main 函数里一把梭。
所以"野脚本"的根源一句话:不是它不会写好代码,是你没告诉它"好代码"在你这儿长什么样。
1.2 它给的 demo 为什么"恰好能跑",却最危险
训练数据里的示例有个共同特点:它们是为了讲清楚一个 API 用法而写的最小片段,默认前提是"全新环境、单次执行、数据已准备好、不用清理"。
这三个默认,在真实项目里全是坑:
- 真实脚本要复用框架已有的 driver 生命周期,不能自己起浏览器;
- 真实脚本要和上百条用例一起、反复、可能并行地跑,不能写死数据、不能依赖执行顺序;
- 真实脚本要自己造数据、自己清理,不能假设环境里已经有那笔订单。
它给的代码在"你手动跑一次"这个场景下完美命中,于是你以为没问题------这是最危险的虚假安全感。等接进回归集,这些脚本会变成 flaky 的重灾区,排查时间比你手写还长。
1.3 还是那个"新同事"模型
测试设计里我用的比喻,在这一步更贴切:
AI 是一个经验极丰富、但今天第一天入职、还没看过咱们代码库的自动化工程师。
你会让一个第一天入职的工程师,不看仓库、不问约定,直接照需求写脚本然后提 PR 吗?不会。你会先让他:拉代码、看目录结构、读 BasePage 和几个已有的测试类、搞清楚登录态和数据怎么造------然后他才动笔。
对 AI 也一样。下面所有技巧,本质都是同一件事:在它动笔前,把"读代码库"这一步给它补上。
二、先看清:新增一条脚本,要躲两类坑
让 AI 新增一条自动化脚本,有两类坑都得盯住,来源不一样、查法也不一样:
- 第一类:AI 生成代码自带的坑 ------ 猜选择器、假断言、绕过封装、重复封装......这是"AI 这把工具"特有的毛病,靠喂上下文 + 人审查治理;
- 第二类:传统自动化本来就有的坑 ------ 时序和页面形态问题(要滚动、有遮罩、在 iframe 里......),人写脚本会踩、AI 默认一样会踩,得在写新脚本时就提前防。
先把这两类坑认全,后面的方法你才知道每一步在防什么。
2.1 第一类:AI 生成代码的六个毛病,当场识破
即使喂了上下文,生成后我仍会逐条查这六点。每一点都配一个一眼能识破的判据:
| # | 毛病 | 长什么样 | 判据 / 改法 |
|---|---|---|---|
| 1 | 脆弱选择器 | 一长串绝对 XPath、div[2]、靠样式 class 定位 |
grep 看有没有 /html/、[数字]、.css-xxxx;改成 role/text/testid |
| 2 | 硬编码 + 死等 | URL/ID/账号写死、time.sleep(3)、force=True 硬点、写死滚动距离 |
grep time.sleep、force=True、固定滚动量和写死 URL;时序问题应靠自动等待、遮罩先消失解决,而不是掩盖 |
| 3 | 绕过你的封装 | 自己 launch() 起浏览器、自己写一遍等待逻辑、定位器直接铺进测试方法 |
看它用没用你给的 page fixture 和 BasePage;grep 测试方法里有没有直接出现 get_by_ / xpath(应只出现在 Page Object) |
| 4 | 假断言 / 没断言 | 只有操作没有 assert,或 assert True、只断言元素存在 |
确认断言对得上用例的"可判定预期"(状态、名额),不是"没报错" |
| 5 | 用例不独立 | 依赖上一条留下的数据、共享登录态不重置、没清理 | 问自己:只跑这一条、连续跑两次,结果一样吗? |
| 6 | 重复封装 | 不看现有文件,又新建一个 EnrollPage / 给同一元素换个名再封一遍、通用动作没进 BasePage |
看它有没有先给"已有复用 / 缺失新增"盘点表;对照现有类名和方法名,同义的必须复用而不是新增 |
第 4 条尤其隐蔽也尤其要命:很多 AI 生成的"绿测试",其实根本没验证任何东西 ------它操作了一通,最后 assert True,或者断言的是"提交按钮还在"这种永远成立的东西。这跟我们在踩坑系列里讲的 assert True 改测试凑绿 是同一颗雷。
这一类坑,责任在"AI 生成"这个动作本身:它有没有真的去核对"用例声称要验证的那条规则"落到了可核对的状态上。
2.2 第二类:传统自动化的老坑,写新脚本时就让它提前防
第二类坑跟"是不是 AI 写的"无关------人写的脚本一样会挂,根源是这些时序和页面形态问题。AI 默认不会主动考虑它们(demo 里元素永远立即可点、没有弹窗)。所以写新脚本时,我会让它在方案阶段就逐条排查、给出对策:
| 传统失败原因 | 为什么会挂 | 让 AI 这么处理 |
|---|---|---|
| 元素要滚动才进视口 | 不在可视区,普通点击报 not in viewport / 元素不可交互 | Playwright 的 click/fill 会自动滚动到元素;Selenium 用 JS scrollIntoView;不要自己写固定滚动量 |
| 被遮罩层/弹窗挡住 | loading 蒙层、cookie 弹窗、悬浮广告盖在目标上 | 先等遮罩消失(wait_for 隐藏)或关掉弹窗,再操作;不要 force=True 硬点------硬点等于没验证可交互 |
| 动画/过渡未结束 | 元素在移动,点击落点不准、被判定遮挡 | 等动画结束/元素稳定再点;框架自动等待一般已覆盖,别用 sleep 凑时长 |
| 元素在 iframe / Shadow DOM 里 | 普通定位找不到嵌套作用域内的元素 | 先切进 frame(Playwright frame_locator;Selenium switch_to.frame)再定位;Shadow DOM 用穿透定位 |
| 点击打开了新窗口/新标签页 | driver 还停在旧页面,后续定位全空 | 显式切换到新页面上下文,或用事件等待新页面,操作完按需要切回/关闭 |
| SPA 路由跳转未完成 | 点完立刻定位新页面元素,页面其实还没渲染 | 点击后等新页面的关键元素/网络空闲,再继续;不要假设点完即达 |
| 列表/下拉动态加载 | 选项是异步请求回来的,立即定位会扑空 | 等加载完成、目标项出现再选;无限滚动要先滚动触发加载 |
| 一个定位器匹配到多个元素 | strict 模式直接报错,或点中了错误的那个 | 收窄定位(加 role+name/父级作用域),保证唯一;不要默默取第一个 |
| 文件上传 / 下载、canvas、验证码 | 非标准 DOM 元素,普通定位和断言失效 | 上传用 set_input_files;下载等下载事件;canvas/验证码这类不适合 UI 自动化的,换接口或打桩验证 |
核心原则:风险在它出方案时就亮出来、给对策,而不是等脚本红了再逐个救火------把传统自动化里最耗人的 flaky 排查,前移成"写之前就设计好"。
两类坑合起来:第一类盯"AI 这把工具别出错",第二类盯"自动化这件事本来的坑别再踩"。下面就讲具体怎么做。
三、基础实操:喂五类上下文,一段提示词跑通
3.1 要喂的五类上下文
我现在让 AI 写脚本,绝不只丢用例。我会按下面五类把上下文备齐(有多少给多少,越全越准):
| # | 上下文 | 具体内容 | 不给会怎样 |
|---|---|---|---|
| ① | 技术栈与版本 | 语言、框架及版本(如 Python 3.11 + Pytest 8 + Playwright 1.45) | 它可能用过时 API、或写出你版本不支持的参数 |
| ② | 框架约定与目录结构 | 测试放哪、POM 放哪、命名规范、配置怎么读 | 它自创目录和命名,跟现有代码格格不入 |
| ③ | 已有可复用资产 | BasePage 封装、已有的 Page Object、conftest 里的 fixture、登录/API client 封装 | 它绕过封装、重复造轮子、自己起 driver |
| ④ | 目标用例 | 要自动化的那条八字段用例(步骤+数据+可判定预期) | 它只能照通用经验编,测的不是你的规则 |
| ⑤ | 真实页面信息 | Codegen 录制的定位器、无障碍快照、相关 DOM 片段(取法见 4.2) | 它只能按业务语义"猜"选择器,可能拟错文案、或编出根本不存在的 testid |
第③类最值钱、也最容易被忽略。 你们框架里那些封装好的"打开页面""等待元素""登录某角色""通过 API 造数据"的方法,正是 AI 最该复用、却最不可能凭空知道的东西。第⑤类则是 4.2 的重点------它"看不到"你的页面,真实定位器得你取出来喂。
3.2 一段结构化提示词
承接上一系列那个「课程报名」的主路径用例,我是这样下指令的(以 Playwright + Pytest + Page Object 为例;换成 Selenium / Cypress / Appium 结构完全一样):
scss
你是我们团队的自动化工程师。请在我们【现有框架】内写脚本,严格复用已有封装,不要另起炉灶。
【技术栈】Python 3.11、Pytest 8、Playwright(python) 1.45,使用 Page Object 模式。
【目录约定】
- 页面对象:pages/,类名 XxxPage,一个页面一个文件
- 测试用例:tests/,文件名 test_xxx.py
- 已有封装:pages/base_page.py 提供 BasePage(含 click/fill/get_text/wait_for 等)
- conftest.py 已提供:page(已打开浏览器的 Page)、login_as(role)(登录态)、
api(后端 API client,可用于造数据)
【已有 BasePage 的方法签名】(必须复用,不要自己重新实现等待)
- click(self, locator)
- fill(self, locator, text)
- get_text(self, locator) -> str
- wait_for(self, locator, state="visible")
【请复用、不要自造】
- 浏览器启停:直接用 page fixture,禁止自己 playwright.chromium.launch()
- 登录:用 login_as("已登录用户")
- 造数据:用 api 创建课程、设置剩余名额,不要依赖页面手工准备
【封装位置与命名】(先看现有文件,沿用现有风格,不要自创一套)
- 跨页面通用动作 → pages/base_page.py 的 BasePage,方法名动词开头(如 click/fill)
- 某页面专属元素和动作 → pages/xxx_page.py 的 XxxPage,一个页面一个文件
- 已存在同页面/同义方法 → 在原文件追加或直接复用,禁止换个名字再封装一遍
【本次要写的用例】
前置:已登录;课程已上架;该用户对该课程无待支付;剩余名额=3
步骤:选择该课程,报名份数填 2,不使用券,提交
可判定预期:生成 1 笔报名单;剩余名额由 3 变 1;不占券
后置:报名单=待支付
【我已用 Codegen 录制好的定位器】(真实页面取出,请直接沿用,不要自己另拟)
- 报名份数输入框:get_by_test_id("enroll-count")
- 提交按钮:get_by_role("button", name="提交报名")
【要求】
0. 动手前先读 pages/ 目录和 base_page.py,盘点已有哪些 Page Object、哪些方法、哪些定位器;
1. 盘点后再决定:已有的直接复用,缺失的才按上面的"封装位置与命名"补进对应文件;
先在 pages/ 下补 enroll_page.py(如不存在),把选择器和动作收进去;
2. 再在 tests/ 下写 test_enroll.py,测试方法只调用 Page Object、写断言,不暴露选择器;
3. 选择器沿用上面 Codegen 录制的定位器;新增定位时按 get_by_role / get_by_text / data-testid 的优先级,禁止绝对 XPath 和靠下标定位;
4. 用 Playwright 内置自动等待,禁止 time.sleep;
5. 数据用 api 在测试里构造、在测试结束后清理;
6. 断言要落到"可判定预期":报名单状态、剩余名额,而不只是"页面没报错";
7. 逐条排查本用例是否存在这些传统自动化常见风险,并在方案里逐项给出处理方式:
元素需滚动才可见、被遮罩或弹窗遮挡、动画未结束、元素位于 iframe/Shadow DOM、
点击后打开新窗口、SPA 路由跳转未完成、列表动态加载、一个定位器匹配到多个元素;
优先用框架自动等待和动作可达性检查,禁止 time.sleep、禁止 force click 硬来掩盖问题。
先不要写代码,先给我一张盘点表:
①已存在、可直接复用的 Page Object / 方法 / 定位器;
②本次缺失、需要新增的(注明放进哪个文件、叫什么);
③你打算复用哪些已有封装、数据怎么造和清理;
④本用例有哪些传统自动化风险点、分别怎么处理。
我确认"没有重复封装、风险有对策"后你再写。
注意最后那段------我让它"先盘点、说方案,再写代码"。 它得先把你 pages/ 里已有的东西摸一遍,确认没有重复封装、缺什么才补什么。这一步能在它动手前拦住 80% 的跑偏,第四章会细讲。
3.3 前后对比:同一条用例,两种产出
不给上下文,它大概率给你这样的"野脚本":
python
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
def test_enroll():
driver = webdriver.Chrome() # 自己起浏览器
driver.get("http://localhost:8080/enroll/123") # URL、ID 写死
driver.find_element(By.XPATH, "/html/body/div[2]/form/input[1]").send_keys("2") # 绝对路径
time.sleep(3) # 死等
driver.find_element(By.XPATH, "//button[2]").click()
time.sleep(3)
# 没有断言,跑完不报错就算过
喂了上下文,它给你的是能直接合入的两段:
python
# pages/enroll_page.py
class EnrollPage(BasePage):
def __init__(self, page):
super().__init__(page)
self.count_input = page.get_by_test_id("enroll-count")
self.submit_btn = page.get_by_role("button", name="提交报名")
def submit(self, count: int):
# 复用 BasePage 的 fill / click(内含统一的等待与异常处理),不直接调原生 API
self.fill(self.count_input, str(count))
self.click(self.submit_btn)
# tests/test_enroll.py
def test_enroll_success(page, login_as, api):
login_as("已登录用户")
course = api.create_course(seats=3, on_shelf=True)
api.ensure_no_pending_enroll(course_id=course.id)
enroll_page = EnrollPage(page)
page.goto(f"/enroll/{course.id}")
enroll_page.submit(count=2)
# 断言落到可判定预期:状态 + 名额,而不是"页面没报错"
assert api.get_enroll_status(course.id) == "待支付"
assert api.get_remaining_seats(course.id) == 1
差别一目了然:后者继承并复用了 BasePage、走 fixture 和 API client、选择器稳、没有 sleep、数据自造自清、断言可判定。这才是能进回归集的资产。
四、进阶:把"合入率"再提一档
4.1 让它"先读再写",而不是"直接写"
新手最容易跳过的就是开头那句"先告诉我方案"。我会把它拆成"先盘点、再说方案":
第 0 步·盘点(先做,否则一定会重复封装): 让它先读 pages/ 目录、base_page.py 和相关已有的 Page Object,列出------现在已经有哪些类、哪些方法、哪些定位器。它不知道你家底的唯一原因,就是没人让它真去看。
盘点之后,再输出四件事:
- 已有的我直接复用哪些(让它点名 BasePage 的哪个方法、哪个 fixture、哪个已有定位器);
- 缺失的才新增/改动哪个 Page Object(跨页面通用进 BasePage、页面专属进 XxxPage,并说明现有文件里确实没有);
- 数据怎么造、跑完怎么清理;
- 本用例有哪些传统自动化风险点、分别怎么处理(对照见 2.2)。
你确认这张"已有复用 / 缺失新增 / 风险对策"的表后再让它写代码。这样你审的是"思路",而不是等它写完一大段、发现重复封装或方向全错再返工。这跟测试设计里"分阶段、人在节点拍板"是同一个心法。
这里还有个很多人忽略的细节:能让它直接读文件,就别手抄签名。 在 Cursor、Copilot 这类接在你代码库里的工具里,直接 @pages/base_page.py、@pages 让它自己打开读------读到的永远是最新真相;而像 3.2 提示词里那样手抄一份方法签名,代码一改就可能和实际不同步,反而误导它。手抄签名只适合一种情况:在纯网页里对话、它读不到你的仓库时,才把关键片段贴给它。
4.2 选择器:它其实"看不到"你的页面
先讲一个很多人没意识到的前提:AI 写选择器时,并没有真的打开你的页面、读取 DOM。 它写下 get_by_role("button", name="提交报名"),依据的是用例里的业务语义 + 训练数据里见过的页面惯例,去推测 "这个按钮合理情况下该叫什么"。所以这些定位器本质是按语义拟的草稿,不是从真实页面读出来的事实------它可能猜错(真实按钮叫"立即报名"),也可能拟了一个根本不存在的 testid。
想让选择器从"猜"变"准",就要给它一双"眼睛",把真实页面信息取出来喂进去,三种来源:
| 来源 | 怎么取 | 给 AI 的东西 |
|---|---|---|
| 无障碍快照 | Playwright page.accessibility.snapshot();或 DevTools 里看元素的 role/name |
元素 role + 可访问名,正好对应 get_by_role |
| 录制生成(最省事) | Playwright Codegen(playwright codegen URL)把操作点一遍;或 Chrome Recorder |
已帮你选好、相对稳的定位器,让 AI 直接沿用 |
| DOM 片段 | DevTools 的 Elements 面板,复制相关元素那段 HTML | 真实标签、属性、文案、有无 id/data-* |
我最常用的组合:开 Codegen 点一遍操作,把它生成的定位器连同用例一起喂给 AI,让它"沿用这些已验证的选择器,只负责按框架组织脚本结构"。选择器是真的、结构是它擅长的,各取所长。
这里还有一条必须跟它说死的纪律:定位器只允许出现在 Page Object 里,测试方法中绝不直接写选择器。 你不强制,它图省事就会把 get_by_role(...) 直接铺进每个测试方法------结果文案一改、元素一动,你得翻遍所有用例去改;而封进 Page Object 后,页面怎么变都只改一处,测试方法调的是 submit() 这种业务动作,不关心按钮到底叫什么。3.2 的要求第 1、2 条就是在卡这件事:Page Object 管"怎么找到、怎么操作",测试方法只管"做什么、断言什么"。
4.3 选择器优先级:不写死,它一定用 XPath
AI 默认倾向给你 XPath,因为 XPath"万能"。但 XPath 恰恰最脆------页面结构一动就挂。我会在提示词里写死优先级:
get_by_role(角色+可访问名)> get_by_text(稳定文案)> get_by_test_id(专门加的测试锚点)> CSS 结构选择器;禁止绝对 XPath(/html/body/...)和纯下标(div2)定位。
如果页面没有任何稳定锚点,正确做法是让前端补 data-testid,而不是让 AI 硬凑一个复杂 CSS。 可以让它在方案里单独列出"建议新增的 data-testid 清单",交给前端------这本身就是一份有价值的产出。
4.4 把三件"用例里没写、但脚本必须有"的事主动交代
测试用例讲的是业务步骤,但自动化脚本要跑得稳,还需要它处理这三件用例里通常没有的事:
- 等待 :明确要求用框架内置的自动等待/显式等待,禁止
time.sleep; - 数据构造:优先用 API 造数(快、稳),而不是用 UI 点一串前置流程;
- 清理:每条用例结束删掉自己造的数据,保证可重复执行、不污染下一条。
这三点你不交代,AI 默认全用"最省事"的写法(sleep、UI 造数、不清理)------因为 demo 就是这么写的。
五、工具和模型怎么选
工具和模型都是放大器,代替不了"喂上下文、先盘点"。 五类上下文喂齐,中端模型就能产出可合入的脚本;什么都不给,最贵的旗舰也还你一段独立 Selenium demo。
工具 ,效率最高的是两个搭配:编辑器里挂一个补全(Copilot 10/月,或国内免费直连的Trae);跨多文件的完整脚本交给agent(Cursor、ClaudeCode,都在20/月这档,能直接 @base_page.py 让它读仓库)。代码不能出内网,就用 Tabnine 本地部署。
模型,写自动化中端就够:日常用 Claude Sonnet / Opus 5.5、GPT-6.1 Sol(性价比高),国产 DeepSeek V4.1、GLM-5.3、Qwen3.8 直连也够用;只有超长重构才需要 Fable 5.1、GPT-6 Astra 这类旗舰。头部模型的编程分早已挤在彼此误差范围内(SWE-bench Verified 已因分数饱和停更),不必追"榜单第一"。
三套照抄搭配:
- 零成本:Trae 或各工具免费档,先把喂上下文这套方法跑通;
- 个人主力:Cursor Pro 或 Claude Code,默认 Sonnet,复杂重构再切 Opus;
- 团队:GitHub 生态用 Copilot Business,代码不出内网用 Tabnine。
写在最后
让 AI 写自动化,瓶颈不在"它会不会写",而在"你有没有把项目的规矩和家底交给它"。 你把它当成第一天入职、需要先读代码库的资深同事,它就能给你能合入的资产;你只丢一句"帮我写个脚本",它就还你一段能跑一次的 demo。
分工也很清楚:AI 负责把用例快速翻译成脚本骨架、把重复劳动干掉;你负责定框架约定、审它复用得对不对、保证断言真的验证了规则。 体力是它的,裁决还是你的。
如果这篇对你有用,欢迎转给那个"AI 脚本写了一堆、却没几条敢放进 CI"的同事。