ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

自动化测试脚本为何总废?十条底层实践指南

自动化测试脚本为何总废?十条底层实践指南 干了十来年自动化测试我最常被问的一句话是“为什么我的自动化测试脚本跑两天就废了”其实大多数时候不是工具不好不是你不努力而是脚本从一开始就没按正确的方法写。最近身边不少朋友在折腾两件事一是用 LangChain 做个 agent让它读测试用例自动生成 UI 自动化测试脚本二是把老项目从 Selenium 迁到 Playwright。工具一直在变AI 也开始介入但一个事实越来越清晰——无论脚本是手写的还是 AI 生成的最后能稳定跑、好维护、坏了好修靠的都是同一套底层实践。所以我想把这么多年写自动化测试脚本踩过的坑、总结出的经验整理成十条最佳实践。这套东西适合刚入行的测试开发也适合被自己脚本坑到怀疑人生的老手。这篇文章不吹工具不整虚的只聊能直接落地的东西。1. 先摸清家底自动化测试脚本为什么总写不好1.1 三个典型症状我接手过不少团队的自动化项目看到的症状高度一致基本可以归成三类。第一类叫“三天绿三天红”。脚本刚写完那几天全绿过了几天开始偶发失败跑十次挂两三次。你去排查发现是某个弹窗偶尔出现、接口慢了一拍、前端动画没结束脚本就报找不到元素。这类问题本质上是等待机制没做好脚本对真实环境的容忍度太低。第二类叫“一改需求就瘫痪”。产品改了个按钮文案脚本挂一片前端重构了 DOM 结构脚本几乎全废。为什么因为每个用例里都写满了绝对 XPath 和页面文本断言页面稍微动一下所有脚本都要跟着改。这种脚本的可维护性约等于零。第三类叫“全绿却漏bug”。脚本跑了两个小时全绿但该验证的核心逻辑根本没验证。很多脚本从头到尾就是“打开页面、点个按钮、看到页面不报错就通过”断言写了一堆但都是形式主义——没断言结果、没断言数据、没断言状态。这种脚本跑得再勤快也保护不了产品质量。这三类症状背后其实是同一个问题写脚本的人把“脚本”当成了“代码”而不是当成了“测试资产”。代码只要运行通过就够了测试资产必须长期稳定、能维护、能追溯。1.2 高效脚本的三个衡量指标在展开十条最佳实践之前我习惯先定义一个“好”的标准。如果一个测试脚本满足下面三个指标我就会认为它是高效的。首先是稳定。同一段脚本在同一环境下重复跑结果波动小。当然没有绝对稳定但要控制偶发失败率在可接受的范围内比如一个由 200 条用例组成的回归集单次运行失败率低于 1%而且每次失败都能给出明确原因。其次是可维护。产品迭代时脚本改动的范围和所需时间可控。这里的关键是抽离和分层定位器抽出来了页面变化只影响一个文件数据抽出来了环境切换只需要改配置步骤抽出来了逻辑复用时不用复制粘贴。第三是可追溯。脚本失败之后能在几分钟内定位到底是产品 bug 还是脚本问题能还原当时页面状态、请求返回、操作日志。那些只能在视频里看到“白屏了一下”的失败是不能接受的。十条最佳实践本质都是为了让脚本往这三个指标靠拢。2. 写脚本前的顶层设计两条最佳实践2.1 实践一测试意图先行用业务场景驱动脚本结构我第一次带团队时犯过一个错让新人拿到测试用例就开始写脚本结果大家写出来的东西风格千差万别有人把登录逻辑塞在每个用例开头有人把断言散落得满屏都是。后来我才想明白写脚本和写代码一样动键盘之前要先做设计。所谓“测试意图先行”就是先想清楚一条用例到底在验证什么业务规则。比如登录这个功能很多人写的脚本是输入用户名、输入密码、点登录、断言出现首页元素。这确实是一条登录用例但业务意图不够完整。如果我要验证“密码错误时提示文案正确”脚本就应该是输入正确用户名、输入错误密码、点登录、断言出现“用户名或密码错误”的提示、断言 URL 没跳转。多了一步“断言 URL 没跳转”就是在验证业务意图而不只是“走过场”。我常用的做法是拿到一条测试用例后先不写代码而是用自然语言把用户路径写一遍。比如“用户从首页进入商品详情页添加购物车去结算提交订单支付成功看到订单状态为已支付”。然后把这条路径上每一步要断言的业务结果列出来比如“购物车数量 1”“订单金额正确”“支付成功后状态变化”。最后再把技术步骤映射上去。现在用 LangChain 这类工具自动生成脚本测试意图更重要了。因为 AI 并不理解你的产品它只能根据你给的用例去“猜”操作路径和断言点。你给的意图越清晰、业务规则越具体生成的脚本质量越高如果你只丢给它一句“测一下登录”它大概率会生成一个能跑通但没什么断言深度的脚本。所以我跟团队说AI 能帮你写骨架但意图必须你来定。2.2 实践二框架选型不将就工具选型是个老话题但永远值得聊。选框架时我主要看三件事团队熟悉程度、项目技术栈、框架本身对测试痛点的解决能力。这里用我常用的三个框架做个对比。维度SeleniumPlaywrightCypress语言生态多语言支持生态最全Python/JS/Java支持多语言以 JavaScript/TypeScript 为主等待机制需要手动管理显式/隐式等待自带自动等待体验顺畅自动等待但不支持多标签页早期版本失败诊断需借助第三方库截图/录像内置 trace 录制、截图、录像自带 time-travel 调试环境兼容浏览器覆盖广桌面端成熟多浏览器、多设备模拟只覆盖浏览器不做桌面端学习曲线平缓但陷阱多稍陡但写起来顺手非常平缓前端团队上手快如果团队是零基础我个人建议优先考虑 Playwright。原因是它把最容易出问题的“等待”做成了默认行为而且内置了 trace 查看器排查问题特别方便。用 Selenium 也不是不行但它把大量细节交给开发者自己处理对经验要求更高。选型还有一个容易被忽略的点不要因为“网上都是这么用的”就选某个框架而要结合你的业务形态。如果你的产品有大量桌面客户端和浏览器交互Selenium 可能更合适如果你们是纯前端团队Cypress 会更友好如果你们要做跨浏览器 UI 回归且团队愿意学习Playwright 是当下性价比很高的选择。框架没有绝对好坏只有合不合适。选完就不要频繁换换一次框架等于重写一半脚本。3. 脚本结构、定位与数据中间三条最佳实践3.1 实践三用PO模式给脚本分层POPage Object Model页面对象模型是 UI 自动化测试里最经典的架构思想核心思路是把“页面结构”和“业务操作”分离。很多新手的脚本长这样登录操作写一堆买东西又写一堆每一步都是page.locator(...)直接操作。一旦页面改了输入框 id所有用例跟着遭殃。PO 模式要求你为每个页面建立一个类把元素定位和页面操作封装进去。比如登录页 LoginPage 负责暴露用户名输入框、密码输入框、登录按钮以及 login(user, password) 这样的方法。测试用例里只调用业务方法不直接出现定位器。用一个简单的 Playwright Python 示例说明class LoginPage: def __init__(self, page): self.page page self.username_input page.get_by_label(用户名) self.password_input page.get_by_label(密码) self.login_button page.get_by_role(button, name登录) def login(self, username, password): self.username_input.fill(username) self.password_input.fill(password) self.login_button.click() self.page.wait_for_url(**/home)测试用例里就是干净的def test_login_success(page): login_page LoginPage(page) login_page.goto() login_page.login(tester, 123456) expect(page).to_have_url(**/home)它的价值不是少写几行代码而是让“页面变化影响最小化”。按钮文案变了只改 LoginPage 一处页面结构变了也只改对应页面的类。PO 之外还有组件对象模型如果页面里有大量复用的组件比如列表、弹窗、导航栏也应该单独抽成类。分层做得越好脚本能存活的版本越多。3.2 实践四选择器要稳定且可读脚本里最脆弱的东西就是元素定位。很多脚本挂掉不是业务逻辑错了而是定位器失效了。我见过最夸张的“绝对定位”长这样/html/body/div[2]/div[3]/div[1]/div[2]/form/div[3]/input这种定位方式只要页面加一个模块就全挂千万别这么写。选择器应该按优先级来能加测试专用属性就加属性其次是靠语义化角色和文本最后才是 CSS 类名和 XPath。在 Playwright 里我推荐的做法是给关键元素加>page.get_by_test_id(checkout-button).click()为什么不推荐纯依赖文本和 CSS文本最容易因为文案调整而变比如“登录”改成“立即登录”get_by_role(button, name登录)就失效了CSS 类名则容易被前端重构改掉。当然使用 XPath 也不是完全禁止处理复杂结构时可以适当用但要遵循“相对 XPath 语义特征”的原则避免复制浏览器生成的绝对路径。我给自己定的一条铁律是写定位器时先问一句“如果有前端同事改了这里的文案这个定位器会挂吗”会挂就换成更不依赖展示层的方式。3.3 实践五测试数据与脚本代码彻底分离另一条反复踩坑的经验是测试数据不能硬编码在脚本里。新手最常见的写法是直接在测试代码里写“张三”“13800138000”“测试账号123”看起来方便但环境一换就全崩。正确做法是把数据放到外部文件或配置里。比如用 YAML 管理不同环境的账号和基础数据test: base_url: https://test.example.com valid_account: username: tester01 password: 123456 invalid_password: wrongpass脚本里通过配置读取。这样从测试环境切到预发环境只需要改配置不用动代码。这是 UI 自动化里最基本的数据管理思路。还要注意数据的“唯一性”。脚本每次运行如果都创建同一条数据第二次跑就会遇到“名称已存在”。我的建议是给关键字段拼时间戳或者 UUID。比如注册用例的用户名可以动态生成import time unique_name fuser_{int(time.time())}另外能用接口构造数据就不要依赖 UI 操作造数。比如测试“订单列表”时最理想的状态是先用接口创建一张符合条件的订单再打开页面验证列表展示。这样不仅快而且隔离了不必要的 UI 干扰用例失败时更容易定位是列表逻辑的问题还是造数过程的问题。数据分离不是一个可以选项而是长期维护的硬要求。4. 等待、重试与失败诊断后三条最佳实践4.1 实践六显式等待是底线精准等待是关键UI 自动化最大的敌人是时序。页面加载有快有慢接口返回有快有慢动画时长还不一定。很多脚本写time.sleep(3)跑起来慢环境一波动照样挂。sleep 是固定的真实世界是随机的所以等待必须是“等条件达到”而不是“等固定时间”。Selenium 的老用户应该都被教育过用WebDriverWaitfrom selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) wait.until(EC.element_to_be_clickable((By.ID, login-button)))用 Playwright 会省心一些因为它自带自动等待click()会等元素可操作后再点。但这不代表你可以完全不管等待比如某个接口请求发出后页面要刷新数据Playwright 默认等的是 DOM 稳定不一定是业务条件满足。这种场景要显式断言业务结果比如等待某个列表项变成目标状态expect(page.locator(.order-status)).to_have_text(已支付)要避免的是“等到元素存在就继续”。元素存在不等于可点击、可见、内容正确。真正的等待策略应该是操作前等可交互操作后等业务结果。把这条想清楚脚本稳定性会提升一大截。顺带说一句用 AI 生成脚本时要特别检查生成的代码里有没有大量sleep生成器最喜欢用它来糊弄时序问题而它恰恰是最需要人工修正的地方。4.2 实践七失败重试要限时限次还要留证据重试机制听起来很实用但也容易被滥用。见过有人把每条用例都重试 5 次结果脚本从 40 分钟跑成 4 小时而且很多真正的 bug 被重试掩盖了。重试的正确姿势是只对“偶发不稳定”的情况重试并且要限制次数第一次失败时先留下证据。我的做法是在用例级别加最多重试两次的配置。第一次失败时立即截图、录制视频、保存 trace然后再重试。第二次失败就不再重试直接判定失败。这样偶发的网络抖动、短暂的服务波动能够被覆盖但持续性的功能性 bug 不会被掩盖。在 pytest 里可以用pytest-rerunfailures插件pytest.mark.flaky(reruns2, reruns_delay1) def test_checkout(page): ...重试的价值不只是“让脚本跑绿”更重要的是配合失败诊断。每一次失败现场都是宝贵的线索页面停在哪个状态、接口返回什么、元素缺了什么。所以重试一定要和日志、截图绑定不能只重试不记录。4.3 实践八断言要具体失败信息要能定位断言是测试的灵魂。很多脚本看起来在断言其实只是“检查页面不报错”。比如expect(page.locator(body)).to_contain_text(成功)这段代码的问题在于页面只要有任何地方出现“成功”两个字就通过根本不知道验证的是哪个业务对象。好的断言应该具体到业务结果。比如验证购物车结算后订单金额order_total page.locator(.order-total).inner_text() expect(order_total).to_equal(¥298.00)断言失败时信息也要足够定位。比如上面这个断言失败你会知道是金额不对但还缺一个“当时页面上到底显示了多少”的信息。所以我写断言时会额外把实际值、页面关键状态、当前 URL 带进日志。不要把断言当成一个“是或否”的开关而要让它成为第一手的现场记录。还有一类断言容易被忽略就是“反向断言”。比如验证“密码错误时不能登录”就一定要断言 URL 没有跳转、没有出现首页元素。很多人只断言了错误提示却没断言“登录没有成功”结果某天错误提示虽然显示了但用户其实已经登录了脚本照样通过。正反两手都断言用例才真正闭环。5. 从代码到工程化质量、CI与持续维护5.1 实践九命名、注释和日志要有“服务他人”的心态脚本不是写给自己看的是写给团队里所有维护者看的。尤其是接手的老脚本如果函数名叫test_1、do_something你会对着它怀疑人生。命名是提高可维护性成本最低的手段。测试函数的命名应该直接表达业务场景和预期结果。比如test_用户登录_密码错误_显示提示且不跳转test_商品加入购物车_购物车数量加一test_订单支付_状态变更为已支付这样的命名哪怕不读代码也能知道你测的是什么。注释也一样我对自己和团队的要求是注释只写“为什么”不要写“是什么”。“点击登录按钮”这种注释纯属废话因为代码已经说明了而“这里不能直接点商品卡片需要先悬停再点否则弹窗不出现”这种注释才是后来人急需的信息。日志和出错的证据链就更重要了。我习惯在每个用例的开始、关键操作、断言处都打日志日志至少包含操作名和关键数据。调试时从日志里就能串出一条完整的执行轨迹。如果团队用的是 Playwright一定要善用它的 trace 功能它能把页面截图、DOM 快照、网络请求、控制台日志全部记录下来排查偶发问题时几乎等于“看了案发全程”。给代码加 Review 的时候也建议把“命名是否清晰”“日志是否足够”“断言是否具体”列进检查清单。5.2 实践十把脚本当成产品持续维护很多自动化项目死在第二个月。原因是脚本写完后没人管产品迭代了脚本没同步环境变了脚本没适配。测试脚本不是一次性的交付物它是一个需要持续迭代的产品。我建议在 CI/CD 流水线里给这些脚本安排明确的执行策略。比如每次代码合并都跑冒烟集每晚定时跑回归集。冒烟集控制在 10 到 15 分钟回归集控制在 1 小时以内。跑完自动归档报告失败时第一时间通知对应负责人并附上失败截图或 trace 链接。这样脚本的价值才真正发挥出来。日常维护也要有节奏。每周花固定时间看一次脚本运行报告重点关注两个数字稳定性和执行时长。稳定性波动说明环境或脚本有问题执行时长增长说明页面在变慢或脚本有冗余等待。同时只要产品需求变了当天就要同步更新对应脚本。脚本和代码不同步比没有脚本更危险——它会给你一种“系统没问题”的错误安全感。产品化地维护脚本还包括定期清理。没有价值的用例、重复的用例、断言空泛的用例该删就删。自动化测试不是用例越多越好而是每一条用例都要有业务价值和清晰断言。6. 常见问题与排查技巧实录6.1 偶发失败先还原现场再谈修复偶发失败是 UI 自动化里最磨人的问题因为它不容易复现你可能盯着屏幕半天它都跑过了第二天又挂了。我处理偶发问题的顺序是固定的先还原现场再做统计最后谈修复。还原现场靠证据。如果脚本有 trace 或视频第一步就是打开它看失败时页面到底发生了什么。我遇到最多的情况是一个异步请求还没返回页面已经进入下一个操作或者某个遮罩层挡住了按钮。没有证据的时候靠猜就是在浪费时间。第二步是做统计。同一用例失败多少次失败时间是否有规律是固定在某一步骤还是随机如果固定在某一步骤大概率是同步时序如果完全随机可能是资源竞争或环境波动。有了统计结果再决定是加等待、加强定位还是使用重试策略。偶发问题最忌讳一上来就加sleep(5)那只是把问题往后推还会拖慢整体执行。6.2 脚本越跑越慢并行化与数据裁剪回归集跑得越来越慢几乎是所有自动化项目的必然趋势。UI 脚本再快一个用例也要几十秒200 条用例串行跑就是两三个小时。很多团队因此放弃了定时回归但慢不等于必须接受可以从并行化和数据入手。并行执行是最直接的提速方式。pytest 可以用pytest-xdist按进程数拆分用例也可以把用例按业务模块拆分成多个任务在 CI 里同时跑多个 job。并行之前要先确保用例之间没有数据依赖或者数据隔离做得足够好否则并行起来互相抢数据、改状态会带来一堆偶发失败。第二个手段是裁剪前置步骤。如果用例只是为了验证订单列表真的不必从注册、登录、下单、支付一路走完。注册和下单可以用接口构造UI 只负责打开列表页做断言。很多人执着于“全链路 UI 操作”认为这样更真实但代价是慢、不稳定、难定位。合理的做法是核心链路走 UI前置造数走接口这是速度与真实性的折中。6.3 当AI开始生成脚本这些实践还成立吗最近用 LangChain 这类工具生成 UI 自动化测试脚本是个热门方向。我自己也试过给它一份测试用例让 agent 读出来再基于 Playwright 生成脚本。坦白说它能快速搭出一个能跑的骨架操作步骤的映射做得不错尤其对于覆盖基本流程非常高效。但问题也很明显。AI 生成脚本时它不会主动考虑“等待业务结果”“数据隔离”“断言业务含义”这些实践。它倾向于生成最直觉的代码比如点击之后立即断言元素存在数据直接硬编码定位器优先使用文本而不是>
返回列表