
做自动化测试这些年我见过太多脚本项目从雄心勃勃走向静默弃坑。最典型的剧本是团队定了个自动化率目标几个人加班加点写脚本前两个月确实跑得欢可只要业务一改版脚本就开始成片变红修脚本的时间比手工测试还长最后整个项目被贴上投入产出比太低的标签。说到底问题不在自动化本身而在很多人把自动化测试脚本当成一次性代码来写而不是当成长期维护的测试资产来经营。这篇文章我打算把高效编写自动化测试脚本的十年经验浓缩成十大最佳实践。不管你是刚接触自动化的测试新人还是正被脚本稳定性折磨的测试开发都能从中找到可落地的思路。每条实践我都会讲清楚背后的逻辑并附上具体代码和踩坑记录。1. 先搞清楚自动化脚本为什么总在烂尾1.1 脚本的本质是测试资产不是一次性代码很多人写自动化脚本时的心态跟写临时脚本处理数据一样能跑就行。这种心态在自动化测试里是致命的。测试脚本和普通代码有个本质区别——普通代码的功能是稳定的接口不变它就不会变但测试脚本面对的是持续演进的产品业务在变、页面在变、数据在变脚本必须跟着变。所以自动化测试脚本的核心诉求从来不是能跑而是能低成本地持续跑。我见过一个项目三个人写了800多个UI用例表面上看自动化率很高可实际上这些用例每次全量回归要跑十几个小时失败率高达40%而且绝大多数失败都是元素定位失效。团队每天都在处理误报真正有价值的缺陷反而被淹没在红色报告里。这个项目最后被叫停不是自动化的错是把它当成了写完就完事的代码。1.2 常见的失败模式与根因以我的观察脚本项目失败基本逃不出这几个根因可维护性缺失元素定位散落在各个用例中没有统一管理页面上一个小改动就得全盘搜索替换。时序处理不当大量使用固定等待脚本跑得快时元素还没加载跑得慢时又超时间歇性失败成了家常便饭。数据耦合严重用例直接依赖数据库里的某几条数据数据一清理脚本就跪。可观测性为零脚本失败后只有一行ElementNotVisibleException没有截图没有日志排查问题全靠猜。基础设施缺失脚本只能本地手动跑没有接入持续集成没有定时执行价值大打折扣。这五大根因恰好对应了我要讲的十大最佳实践。下面逐一拆解。2. 十大最佳实践详解从能跑到能维护2.1 动手前先给功能做个自动化体检不是所有功能都适合自动化这是很多人忽略的第一条最佳实践。适合自动化的功能通常有三个特征业务逻辑稳定、回归频率高、数据可构造。比如登录、搜索、订单流转这类核心流程每次发版都要验证自动化收益非常高而那种一个月才用一次的报表导出或者还在频繁改交互的原型页面自动化纯属给自己找麻烦。我在每个自动化项目启动前都会做一个评估清单逐项打分评估项高优先级低优先级业务稳定性近3个月没有大的流程变更正在频繁改版回归价值每次发版都必须验证只在季度末使用数据准备难度可通过API或SQL快速构造需要复杂手工造数脚本维护成本页面结构稳定、元素易定位大量动态组件、iframe嵌套按这个清单筛过之后再决定自动化覆盖范围比盲目追求覆盖率要实际得多。覆盖率不是目标稳定的核心回归集才是。2.2 用页面对象模型把业务逻辑和操作细节隔离页面对象模型Page Object ModelPOM是我认为UI自动化里回报率最高的一项实践也是区分专业脚本和业余脚本的分水岭。它的核心思想很简单把页面长什么样和用户能做什么分开。举个例子登录页有个用户名输入框普通写法是每个用例里直接写page.fill(#username, admin)。这看起来没什么问题可一旦开发把#username改成#user-name你就得在所有涉及登录的用例里逐个修改。POM的写法是把这些定位信息收进一个登录页面类对外只暴露login(user, password)方法用例里根本不关心输入框长什么样只调用方法即可。页面改版时你只需要改一个类文件。# pages/login_page.py class LoginPage: def __init__(self, page): self.page page # Playwright的Page对象 def login(self, username, password): self.page.get_by_test_id(username).fill(username) self.page.get_by_test_id(password).fill(password) self.page.get_by_test_id(login-btn).click() self.page.wait_for_url(**/dashboard)POM的好处一箭三雕定位信息集中管理、业务操作可复用、用例可读性大幅提升。读用例的人看到的是登录→下单→支付这样的业务语言而不是在id为xxx的输入框输入xxx这种操作流水账。2.3 数据驱动让脚本从代码变成表格所谓数据驱动就是把测试数据从代码里抽出来放到外部文件JSON、YAML、Excel里脚本只负责执行数据负责提供输入和期望结果。这样做的好处是新增用例不需要写代码只需要在数据文件里加一行。我常用的做法是结合pytest的参数化机制。比如登录测试我准备一组测试数据每组包含账号、密码、期望提示信息代码只有一份但能覆盖十几条用例路径。# test_login.py pytest.mark.parametrize(case, load_test_data(login_cases.json)) def test_login(self, page, case): login_page LoginPage(page) login_page.login(case.username, case.password) if case.expected_success: expect(page).to_have_url(**/dashboard) else: expect(page.get_by_test_id(error-msg)).to_have_text(case.expected_error)数据驱动让自动化测试脚本的维护成本大幅下降。产品经理调整规则改数据文件。新增一个异常场景加一行数据。整个团队不需要都会写代码只要懂业务、会填表就能添用例。这在团队协作中价值极大也方便测试用例与自动化脚本之间建立一一对应关系。2.4 告别固定等待掌握显式等待和自动等待脚本不稳定最经典的症状就是在我电脑上都能过一上CI就挂。绝大多数情况都是时序问题脚本执行到某个步骤时页面元素还没渲染完。新手最常见的解决办法是time.sleep(3)先睡个几秒再说。这种做法我强烈反对因为固定等待有两个致命问题一是机器性能波动时等待时间不可控二是每多一次sleep整个用例执行时间就被拖长一大截。现代测试工具其实都提供了成熟的等待机制。Playwright有个很棒的设计叫自动等待所有操作都会自动等待元素可操作默认情况下你根本不需要写sleep。Selenium里则用显式等待明确指定等待条件。# 显式等待的三种形态 element page.get_by_test_id(submit-btn) # Playwright自动等待 page.wait_for_selector(.loading-mask, statehidden) # 等待元素消失 expect(page.get_by_test_id(toast)).to_be_visible(timeout5000) # 断言式等待我的经验是等待条件要跟业务语义对齐。等待加载中遮罩消失比等提交按钮出现更靠谱因为前者消除的是整个异步状态后者可能只是按钮先渲染了但数据还没回来。时序逻辑涉及到前后端交互理解了这一点很多间歇性失败就能从根上解决。2.5 选择器策略稳定是第一优先级元素定位是UI自动化里最容易翻车的环节。很多脚本写的时候图省事直接拷浏览器的xpath路径结果是一串/html/body/div[3]/div[2]/div/div[1]/div/div[2]/form/input。这种xpath在页面结构稍有调整时立刻失效而且没有任何可读性完全是技术债。我给团队定的选择器优先级是这样的可读的专用测试属性># 轻量级重试装饰器 def flaky(max_attempts3, retry_interval2): def decorator(func): def wrapper(*args, **kwargs): for attempt in range(1, max_attempts 1): try: return func(*args, **kwargs) except Exception: if attempt max_attempts: raise logging.warning(fAttempt {attempt} failed, retrying...) time.sleep(retry_interval) return None return wrapper return decorator重试策略有几个要点一是重试前必须重置状态比如清空购物车、登出重新登录二是重试次数要克制一般2到3次足够超过这个数就不是环境抖动而是真有问题了三是每次重试之间的间隔要有退避避免在系统还没缓过来的时候连续冲击。合理使用重试能让脚本稳定性提升一个量级同时又不会掩盖真实的缺陷。2.8 并行执行用资源换时间自动化用例一多执行时间就成了大问题。800个用例顺序跑每个5分钟一跑就是几十个小时根本没法作为发版前的快速反馈。并行执行是必须的。并行执行分两个层面多进程并行和分布式执行。以pytest和Playwright为例最简单的方案是pytest-xdist一行命令就能按进程数拆分用例。pytest tests/ui -n 8 --distloadscopePlaywright官方也支持workers的概念通过pytest --workers4可以控制浏览器实例的并发数。但并行执行有个很重要的前提用例之间必须隔离。如果用例A创建了一条订单数据用例B假设数据库里只有一条订单两个用例并行跑起来就会互相踩。所以并行之前我会先做两件事用例数据的独立性和用例间的执行顺序解耦。只有底层的用例设计足够好并行才是安全的否则就是在给失败率添柴火。2.9 测试脚本也要评审和版本管理很多团队的自动化脚本是游离在版本管理之外的要么没有仓库要么在仓库里但没有分支策略和评审流程。这在我眼里是很大的隐患。脚本也是代码而且是跟业务代码直接打交道的代码业务代码可以改得一团糟测试脚本同样可以。我建议把测试脚本和业务代码放在同一个仓库里至少也要是同一个组织下的独立仓库并建立同样的代码评审制度。评审的检查点我一般关注这几项选择器是否稳定有没有使用绝对路径xpath有没有不必要的固定等待用例是否声明了所依赖的测试数据以及清理方式日志是否足以支撑失败排查是否引入了真实的超时和重试配置评审的意义不仅在于把关质量更在于让团队成员都熟悉其他人的用例避免这脚本只有作者自己能维护的窘境。脚本的维护者不应该是一个人而应该是一个团队。2.10 把脚本接入CI让自动化真正运转起来最后这条实践也是我认为让自动化测试真正产生价值的一环——接入持续集成。脚本如果只能由人手动触发那它本质上是个玩具。真正的价值在于每次代码提交、每个夜间构建、每次上线前自动化的用例都能自动跑起来并且把结果推送给对应的人。在实践层面我通常做三件事按触发场景拆分任务冒烟用例提交触发控制在10分钟以内、核心回归夜间构建全覆盖慢用例、上线用例发布前在预发环境执行。统一管理环境变量测试环境地址、账号、数据库信息全部通过CI的环境变量注入不写死在脚本里。换一套环境跑自动化只需要改配置不需要动代码。结果通知推送到即时通讯工具失败时附带失败率、失败用例列表和报告链接。让团队在第一时间感知问题。接入CI之后自动化脚本才从局部工具变成了基础设施。我也见过很多团队把这步做得很好比如每次合并请求自动跑受影响业务的回归5分钟内给出结果开发同学合代码前心里有底测试同学的重复工作大幅减少。3. 实操案例用Playwright从零搭建一套可维护的UI脚本3.1 项目背景与框架选型理论讲了不少我用一个实际案例把上面的最佳实践串起来。假设要给一个带登录的商品后台系统写自动化回归脚本核心流程是登录、创建商品、查询商品三件事。技术选型上我选了Playwright Python pytest原因是Playwright内置自动等待和视频录制自带浏览器级别的隔离机制能省掉不少框架层的工作。目录结构我会这样组织ui_tests/ ├── configs/ │ ├── __init__.py │ └── env.yaml # 环境配置不提交进仓库 ├── pages/ │ ├── base_page.py # 页面基类 │ ├── login_page.py │ └── product_page.py ├── test_data/ │ ├── login_cases.json │ └── product_cases.json ├── tests/ │ ├── conftest.py # 共享fixture浏览器、登录状态、失败截图 │ ├── test_login.py │ └── test_product.py ├── reports/ # 自动生成gitignore └── pytest.ini这个结构本身就是最佳实践的缩影页面对象、测试数据、测试用例三层分离谁也不会因为其他层的变化而无谓变动。3.2 核心代码实现PO封装与环境隔离先看页面基类我把公共操作都收敛到这里。class BasePage: def __init__(self, page): self.page page def goto(self, url): self.page.goto(url, wait_untilnetworkidle) self.logger.info(fNavigated to {url}) def screenshot_on_failure(self, name): path freports/screenshots/{name}_{int(time.time())}.png self.page.screenshot(pathpath)登录页面继承基类将页面上所有定位信息集中在类内部。class LoginPage(BasePage): def __init__(self, page): super().__init__(page) self.username page.get_by_test_id(username) self.password page.get_by_test_id(password) self.login_btn page.get_by_test_id(login-btn) def login(self, username, password): self.username.fill(username) self.password.fill(password) self.login_btn.click() self.page.wait_for_load_state()用例层只写业务意图不碰任何定位细节。# conftest.py pytest.fixture def logged_in_page(page, env_config): login_page LoginPage(page) login_page.login(env_config.admin_user, env_config.admin_password) return page然后数据文件里维护所有登录场景。这样整套脚本的可维护性就体现在前端改了元素结构只改LoginPage加了测试场景只改JSON数据换套环境跑只改env.yaml。每类变更都只会动到一小块地方改动的风险面被压缩到最小。3.3 运行效果与维护心得实际跑起来的效果是全套80个核心回归用例开4个worker并行13分钟跑完失败率稳定在5%以内。这5%的失败也基本都不是脚本自身问题而是第三方服务偶发超时通过重试机制基本能消化掉。维护心得方面我最想强调的是少即是多。很多团队喜欢堆用例我反而建议用例数量克制优先保证每个用例的业务价值。那些覆盖同一个逻辑但改动频繁的细碎用例往往是最耗维护精力的地方。每写一条用例前问自己一句这条用例给我带来的回归信心能抵得上后续半年的维护成本吗想清楚这个问题脚本质量会高很多。4. 常见问题与排查技巧实录4.1 高频问题的排查思路自动化脚本跑了一段时间后遇到的问题其实高度趋同。我整理了一份高频问题速查表按这个表格逐项排查绝大多数问题都能快速定位。症状可能原因排查方法用例偶发失败重试能过异步时序问题或环境抖动看视频回放确认是操作过快还是元素未加载某一天全部用例都挂环境被重置或账号被踢用API探活检查环境看登录态是否失效只有CI上挂本地正常分辨率/窗口尺寸差异、等待时间不足统一浏览器视口设置核对CI与本地配置元素定位报错频繁页面改版或动态属性变化用开发者工具检查元素是否仍在更新PO层并行执行时用例互踩用例数据不隔离检查用例是否共享状态改用独立随机数据4.2 排查问题的标准路径三步定位法遇到神秘失败我的标准动作是三步走。第一步看日志。日志能把执行过程还原到操作级别通常能看出是卡在第几步以及这个步骤之前发生了什么。第二步看截图和视频。自动化的现场记录告诉我点击时页面的真实状态很多时候原因一目了然某个弹窗遮挡了按钮、某个下拉框没有展开、某个值没有回填。第三步做孤例复现。把用例单独拎出来跑三遍如果孤例能稳定通过基本就锁定是并发或相互依赖的问题如果孤例也挂那就是用例本身的时序或数据问题。这套三步定位法帮团队把单条用例的平均排查时间从半小时压缩到了15分钟以内。排查效率的提升间接也是脚本稳定性的提升因为定位快了修复的意愿才高。很多脚本项目烂尾不是缺陷多而是修一个缺陷的时间成本太高大家选择放弃。5. 下一步AI Agent会把自动化测试脚本带向何方5.1 从写脚本到生成脚本LangChain Agent的思路最近行业里讨论比较多的一个方向是借助大语言模型和Agent架构来自动生成自动化测试脚本。我也在关注一个典型的技术路径基于LangChain开发一个Agent输入是自然语言或结构化描述的测试用例输出是可直接执行的UI自动化脚本。这个Agent的典型工作流程是先用LLM解析测试用例文本提取业务步骤和预期结果然后检索被测系统的元素映射把业务步骤映射为具体的页面操作再通过工具调用生成代码比如生成Playwright的Python代码最后甚至可以驱动浏览器在测试环境试跑一遍根据失败反馈自动修复脚本。这个生成→执行→报错→修复的循环正是LLM比较擅长的事情因为它需要的是模式识别和代码生成而不是复杂的逻辑推理。我看到的几个团队验证的结论是对结构标准、页面元素有良好测试属性的系统AI生成脚本的准确率已经相当可观尤其是增删改查这类CRUD流程的用例生成质量很高。但如果页面埋点不规范、业务规则复杂、数据构造依赖很高AI生成脚本的调试成本依然不低甚至可能比手写还高。5.2 落地建议先把可读的用例和稳定的元素准备好我的判断是AI不会让测试工程师失业但它会重塑工作内容。未来写脚本的技能中知道怎么选合适的选择器知道怎么设计稳定的用例知道怎么判断失败原因是线上缺陷还是脚本飘测这些判断力反而更加重要。AI能把重复的编码劳动替代掉但解决不了业务复杂性和系统不可测的问题。所以如果你也想拥抱这个方向我建议先从以下三件事入手把测试用例结构化AI理解固定的模板远比理解自由文本容易统一前置条件、操作步骤、预期结果三段式描述。推动前端建设测试属性>