ARTICLE DETAIL

资讯详情

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

自动化测试五大陷阱:定位器、数据、断言、稳定性与分层

自动化测试五大陷阱:定位器、数据、断言、稳定性与分层 提到自动化测试我估计不少开发者的第一反应都是“这玩意儿我熟不就是写脚本、跑用例、出报告嘛”。可真到了项目里你会发现情况完全不是这么回事——脚本今天能跑明天就挂用例越写越多维护时间比写业务代码还长好不容易全绿了上线还是出问题。我在好几个团队里见过同样的剧情反复上演自己也踩过不少坑所以特别想把这五个最典型的陷阱摊开来聊一聊。这篇文章里的内容全部来自我在真实项目里的实操复盘涉及pytest、selenium、appium、allure这些常用工具也包含接口自动化、UI自动化、测试数据管理这些绕不开的环节。不吹不黑不绕弯子每个陷阱我都会说清楚它为什么容易犯、会带来什么后果、正确的做法是什么。不管你是刚进测试这个门的新人还是写了很多年用例的“老油条”只要你还在跟自动化测试打交道这篇文章应该都能帮你少走点弯路。1. 自动化测试越做越累根源往往不在技术上聊五个具体陷阱之前我想先说说一个整体感受很多团队自动化测试做不起来或者做起来了却养不住根本原因不是什么技术选型不对、工具不够好而是从一开始就把自动化测试理解偏了。1.1 大部分人对自动化测试的预期是错的我遇到过的团队最常见的心态是“自动化测试 帮我省事”。老板想的是“有了自动化就不用养那么多测试人员了”开发想的是“提测之后让脚本自己跑我就可以去写新功能了”甚至连测试团队自己都有这种错觉觉得只要用例跑起来自己就“进阶”了。但自动化测试本质上不是“省事”而是“把确定性的检查交给机器把不确定性的判断留给人”。它不会减少测试工作只会把工作形态从“重复点击页面”变成“设计场景、维护脚本、分析报告”。那些一上来就追求100%自动化的团队往往会在三个月后痛苦地发现用例库膨胀了稳定性崩了维护成本超过了手工测试。这个现象不是个例几乎每个从零开始搞自动化的团队都会经历一次。1.2 五个陷阱其实是五个决策失误我复盘了自己和身边同事踩过的坑发现最终都可以归结到五个关卡上。第一关是写脚本时的“定位与等待”代码能不能稳定地找到元素、等到该等的状态第二关是“测试数据”每条用例是不是真的干净独立第三关是“断言”你到底有没有真的验证了业务还是在自欺欺人第四关是“稳定性治理”用例多了之后偶发失败你到底怎么处理第五关是“测试分层”你投入了80%精力做的自动化是不是真的在最有价值的那一层。这五个问题不是孤立的它们会互相放大。定位不稳让用例偶发失败你就怀疑是数据问题数据不干净导致断言失败你又怀疑是环境问题环境问题多了你就开始讨厌整个自动化体系。所以下面我会逐个拆开讲每个陷阱给出具体的现象、原因和可落地的解法而不是只讲大道理。2. 陷阱一定位器和等待时间全靠“猜”和“赌”这是自动化测试里新手最容易犯、也最容易被忽视的问题。我看了太多同学的脚本看起来跑得挺顺畅实际上随时可能“翻车”只是还没到时候。这类脚本的共同特征就是定位器写得特别“脆”等待时间写得特别“死”。2.1 “脆”定位器的代价UI改一像素脚本就罢工先说定位器。很多同学写selenium脚本的时候特别喜欢用浏览器里“右键复制XPath”的绝对路径类似这种// 反例不推荐 driver.find_element(By.XPATH, /html/body/div[3]/div[2]/form[1]/div[5]/input[2]).click()这种定位器看着能用但本质上是在赌“DOM结构永远不变”。哪怕只是页面顶部多了一个banner、登录框前面多包了一层div这个XPath就从“有效”变成“404”。我见过最极端的情况是产品经理改了一下按钮的文字从“提交”改成“保 存”结果整个回归用例挂了二十多条——因为脚本里还有用文本定位的# 反例不推荐按钮文案一变就全挂 driver.find_element(By.XPATH, //button[text()提交]).click()更让人头疼的是用index定位的//div[2]/div[1]/div[3]/button前面每多一个广告位、每少一个推荐模块后面的序号就全错位了。对于这类问题我的经验是要把定位器当成接口契约来设计。优先使用带业务含义的稳定属性比如># 推荐稳定属性 统一管理 LOGIN_BUTTON {by: By.CSS_SELECTOR, value: [data-testidlogin-submit]} def click_login(): locator LOGIN_BUTTON[value] driver.find_element(By.CSS_SELECTOR, locator).click()如果页面结构实在没有稳定的属性宁可写相对XPath从某个稳定祖先节点开始往下找也不要用全路径。“脆”定位器改一次的代价不只是一行代码而是要跑一遍全量回归来确认“改对了”这个成本往往被大家严重低估。2.2 sleep等待时好时坏的“薛定谔稳定”定位器之后就是等待。我早期写自动化也是这个路子点完按钮不知道什么时候出结果于是一拍脑袋写个sleep(3)先睡三秒再说。跑了几次发现“有时候三秒不够”就把sleep改成了sleep(5)再不稳定就sleep(8)。用例最终是稳定了但代价是每条用例跑得好慢几百条用例一次回归下来光是等待时间就占了执行时长的大半。sleep最大的问题不是它没用而是它把“时间”当成了“条件”的唯一变量。网络快的时候等两秒是浪费网络慢的时候等五秒还不够。它不考虑页面到底加载到什么程度只是无脑阻塞。不管是time.sleep()还是implicitly_wait(10)这种隐式等待本质上都是对真实场景的偷懒。隐式等待还有个隐藏bug它会对driver整个生命周期内所有找元素的行为生效一旦某个元素就是不出现它会一直等到超时才抛异常一个用例里几十次find_element最坏情况下每次都要等满超时时间效率低到让人怀疑人生。2.3 把“轮询条件”当作测试的一部分我现在的标准做法只有一个显式等待用WebDriverWait配合expected_conditions把“等到目标状态出现”这件事明确写出来。比如等待一个弹窗出现from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 推荐显式等待轮询直到弹窗可点击 WebDriverWait(driver, 10, poll_frequency0.5).until( EC.element_to_be_clickable((By.CSS_SELECTOR, [data-testidconfirm-dialog])) )这里面的10是总超时秒数poll_frequency0.5是每0.5秒去检查一次元素状态。好处很明显页面1秒加载完了脚本就1秒后继续页面6秒才加载完脚本就6秒后继续。它不浪费不必要的时间也不会因为时间不够而误报失败。等待的本质逻辑我自己的理解是这样的——自动化脚本永远不该假设“固定的时间足够”而应该假设“目标状态一定会在某段时间内达成我只需要轮询就好”。这点思维转过来之后脚本稳定性会有一个质的飞跃。注意显式等待里poll_frequency别设太小默认0.5秒已经够用设成0.01秒除了把CPU跑满并不会让脚本更快。3. 陷阱二测试数据“随手造、跑完扔”污染从第一天就开始了定位器和等待时间解决的是“脚本能不能找到元素”的问题但脚本找到了元素之后它操作的“数据”又是一门大坑。我可以很负责任地说很多团队自动化用例跑得不稳定一半以上的根因都出在测试数据上而不是脚本本身。3.1 共用数据导致的“你方唱罢我登场”我见过一个非常典型的场景一堆用例共用一个登录账号A用例在购物车里加了商品B用例跑的时候发现购物车里多了一件不该有的商品于是断言失败。这其实不是代码问题也不是脚本问题而是数据被用例间共享了数据之间互相踩脚。类似的情况还有用例A创建了一笔订单用例B去统计“今天的订单数量”发现比预期多了一笔用例A把用户资料改成了“北京市”用例B断言“默认地址是上海市”直接失败。这些现象很容易被误判成“环境不稳定”或者“脚本写错了”于是测试同学疯狂加sleep、加重试结果问题依旧。根子就在于每条用例没有自己独立的数据边界。3.2 正确做法是“用例前造数、用例后清理、数据唯一化”我的标准实践是三条原则第一不在用例执行前依赖环境里“恰好存在”的数据而是主动通过接口或者数据库把数据造好第二每条用例使用的数据要唯一最常用的手段是时间戳或者随机后缀比如注册一个test_user_202501121030这种账号绝对不会跟任何其他用例撞车第三用例结束后要清理或者至少把数据标记成“已过期”避免影响后续用例的统计断言。造数方式的优先级我踩过几轮之后有了明确排序接口造数 数据库SQL造数 UI造数。# 推荐用接口造数代替UI造数速度快且稳定 import requests def create_order(user_token: str, amount: int) - str: resp requests.post( https://api.example.com/orders, json{amount: amount}, headers{Authorization: fBearer {user_token}}, ) assert resp.status_code 201, resp.text return resp.json()[order_id]用接口造数发起一个HTTP请求几十毫秒就完事用UI造数得登录、跳转、填写表单、点击提交可能几十秒甚至更久而且还额外引入了定位器稳定性问题。数据清理则优先在用例结束时通过接口删除如果接口没有删除能力再考虑直接操作数据库。实在不方便清理的数据就加一个create_time之类的标记字段把历史数据对当前用例的干扰排除掉。3.3 环境数据漂移最隐蔽的断言杀手前两种数据问题还比较明显下面这个更隐蔽你依赖了一个共享的“测试环境”但这个环境不是只有自动化在跑手工测试也在用、开发也在用甚至其他团队的自动化也在用。你以为环境里的数据是稳定不变的实际上它每一分钟都在被人改。这种环境数据漂移造成的失败最气人——脚本跑到一半数据被某个不知道谁改掉了断言失败然后你重新跑一遍又好了。来回几次大家就会慢慢形成一种“失败重试就好”的坏习惯而不会再去找根因。我的建议是如果条件允许给自动化准备一套独立的环境如果做不到至少要保证自动化用例操作的数据范围比如特定前缀的账号、特定的测试商户是其他渠道约定俗成不去碰的“独立测试域”。注意凡是发现用例“重试就跑过不重试就失败”先别急着加retry先去查数据。重试只是把问题藏起来数据问题不解决用例会永远在“稳定和碰运气”之间反复横跳。4. 陷阱三断言要么不写要么写成“抓全屏”定位和数据保证了脚本能稳定地走完流程但如果断言写得不对脚本走完流程也等于白走。这是我认为所有陷阱里最讽刺的一个很多人辛辛苦苦写自动化最后却败在“根本没有有效验证业务”这件事上。4.1 无断言脚本自动化跑了个寂寞有一种用例跑起来全绿但你仔细看代码从头到尾只有“点击、输入、点击、输入”一个assert都没有。这种脚本的逻辑是只要没报错就算通过。可问题是没报错只能说明元素被找到了、点击动作执行了根本不能说明业务结果是对的。比如你做了一个“提交订单”的操作按钮点了页面也没报错但订单其实没创建成功——这种用例会稳稳当当给你一片绿直到线上出了事故你才发现自动化根本就没拦住。写断言最基础的要求是每一个关键业务动作之后至少有一个对应结果的校验。提交订单之后校验订单列表里出现了这条订单修改资料之后校验页面上显示的是新资料发送消息之后校验收件人真的收到了。如果UI上不好验证就去查数据库、查接口返回值总之必须有“结果校验”这一步。4.2 断言写得过多过死改个文案就是一场灾难另一头就是断言太狠。我见过有同学断言整页文本直接把driver.find_element(By.TAG_NAME, body).text抓下来然后跟一个包含了所有页面文字的期望串做assert equals。这种断言方式只要页面加一行字就会失败哪怕这行字跟业务一点关系都没有。还有人对弹窗文案做逐字匹配产品把“确定”改成“OK”就直接挂一片。断言的颗粒度要跟业务风险匹配不能是“越细越好”。我一般分三层来写状态层接口返回的状态码、UI上关键组件是否可见这是最基础的业务层关键业务字段是否符合预期比如订单金额、用户名称、消息内容数据层直接校验数据库或接口返回的数据结构比如一项任务是否真的落库了# 推荐分层断言只验证关键信息 def test_create_order_success(): order_id create_order(amount99.9) # 状态层订单创建接口返回成功 assert order_id is not None # 业务层通过查询接口确认订单信息 resp retrieve_order(order_id) assert resp[status] CREATED assert resp[amount] 99.9 # 数据层数据库确实有这条记录 row query_db(SELECT * FROM orders WHERE order_id ?, (order_id,)) assert row is not None4.3 过度依赖UI断言的成本不划算还有一类断言问题不那么明显但影响很大就是不管什么都想通过界面去验证。UI断言本身是成本最高的断言方式——查找元素要时间渲染要时间等待要时间而且极易受到样式变化的影响。我在项目里发现很多页面上的“文本展示结果”后端接口早就已经把同样的数据返回给前端了你完全可以直接断言接口返回。所以我现在的一个原则是能用接口断言就用接口只有“端到端用户真实操作”的场景才保留UI断言。注意断言不是越多越好也不是越严越好。好的断言是“刚好能证明这件事做对了而且需求变化时不用反复改”。5. 陷阱四用例一多就“飘红”稳定性全凭运气自动化做了一阵子之后用例数量上来了新的噩梦也开始了——今天挂两条明天挂五条重新跑一遍又全过。这种偶发失败的用例行业里叫flaky test。它比其他任何问题都更消耗团队耐心因为它看起来毫无规律可寻连“从哪里开始排查”都让人头大。5.1 用例之间的隐式依赖顺序一换结果就变我最开始写pytest用例的时候完全没有用例顺序的概念总觉得每条用例都应该是独立的。但实际跑起来发现用例A创建了账号用例B拿这个账号去登录并断言“首次登录赠送优惠券”这套流程单独跑没问题但如果你把用例B跑到用例A前面它就直接失败。这不是代码bug而是用例之间有隐式依赖只是我们没写出来。要治理这种问题一是要让用例真正独立A需要的数据由A自己准备B需要的数据由B自己准备谁也不依赖谁二是跑测试的时候刻意“打乱顺序”跑几轮在pytest里可以装pytest-random-order插件随机排序跑下来如果出现诡异的失败十有八九存在隐式依赖。这个手段非常有效强烈建议团队里都配上。5.2 并发执行的资源竞争你抢了我的端口和数据库用例多了之后大家自然会想到并行执行来提速pytest里可以用pytest-xdist插件的-n auto参数selenium也可以开分布式。但并发带来的一个巨大坑就是资源竞争两个用例同时去创建订单结果都用同一个mock端口或者多个用例同时写同一个测试账号的数据其中一个把另一个的登录态顶掉了。解决并发竞争的基本思路是“资源隔离”每个worker进程分配独立的测试数据域按worker编号编号唯一前缀端口动态分配而不是写死数据库表尽量按功能模块分散条件允许的用独立库。这里的排查思路一般是先确认同一组用例在-n 1顺序执行时是否100%通过如果顺序执行稳定而并发执行不稳定那基本可以断定是资源竞争问题跟业务代码关系不大。5.3 用重试机制兜底但永远要记录失败原因当然现实世界里哪怕脚本写得再规范偶尔也会因为网络抖动、第三方服务临时不可用导致失败。这时候合理的对策是“重试”但重试必须有条件、有记录、有上限。# pytest配置失败自动重试2次间隔1秒并保留allure报告 # pytest.ini [pytest] addopts -p no:cacheprovider --reruns 2 --reruns-delay 1--reruns 2表示失败后最多重跑2次--reruns-delay 1表示重跑前等1秒。前提是数据清理逻辑做得够好否则重跑的时候数据已经破坏了重试多少遍都会失败。另外每次失败和重试的记录都必须进到allure报告里方便事后统一分析。如果发现某个用例频繁触发重试一定要把它摘出来专项治理而不是靠重试一直兜着。注意重试是“止血”不是“治疗”。如果一条用例重试成功率高到离谱说明它本身不稳定必须找到根因。否则你只是在一遍遍地放大真实问题的隐藏概率。6. 陷阱五眼里只有UI自动化把金子都埋在土堆里前四个陷阱讲的都是“怎么把脚本写好”这第五个陷阱要跳出来聊一聊“该把功夫花在哪里”。我见过太多团队一上来就奔着UI自动化去投入了最多的人力、资源和时间最后维护成本大到几乎把团队压垮。6.1 测试金字塔不是理论是无数团队用血泪换来的测试金字塔讲的是从下往上的三层单元测试数量最多、接口/服务测试数量次之、UI/端到端测试数量最少。这个比例背后是稳定的经济学逻辑越靠近底层执行越快、稳定性越好、维护成本越低越靠近上层执行越慢、越脆弱、维护成本越高。一个简单算术就能说明问题接口用例如果80%的耗时都花在等待和定位上它的成本天然就比纯接口请求的验证高出一个数量级。这里我不是反对UI自动化UI自动化有它不可替代的价值——它验证的是用户真实视角的完整链路。但UI自动化应该用来覆盖核心主流程、支付路径、登录注册这类高风险场景而不是把所有功能点都做成UI用例。比如“修改昵称”这种操作接口层已经能断言“修改成功 数据库同步更新”根本没必要在UI层再点一遍。6.2 接口自动化的性价比高得惊人我在自己的项目里接口自动化和UI自动化用例的比例大致是7:2:1。接口层是性价比最高的投入方向写起来快、跑起来快、定位问题快。配合pytest和requests一套基于数据驱动思想的接口测试框架并不复杂import pytest import requests # 数据驱动测试数据用参数化统一管理 pytest.mark.parametrize( payload, expected_status, [ ({user: alice, amount: 100}, 201), ({user: , amount: 100}, 400), ({user: alice, amount: -1}, 422), ], ) def test_create_order_validation(payload, expected_status): resp requests.post(https://api.example.com/orders, jsonpayload) assert resp.status_code expected_status这种用例一次能跑几十条速度飞快而且完全不依赖UI渲染稳定性极高。相比之下同样数量的UI用例可能十台机器要跑一个小时还要忍受各种偶发失败。所以如果团队资源有限我的建议永远是把接口自动化的优先级排在UI自动化前面。6.3 移动端自动化更要把精力放在“核心链路”上热搜里有很多App自动化相关的关键词appium、adb等从大家搜的东西能看出很多团队已经在做移动端自动化了。App自动化比Web端更麻烦需要真机或模拟器环境、需要处理权限弹窗、需要应对不同机型的分辨率适配。所以在App端我尤其建议“少而精”只覆盖最核心的下载、启动、登录、付款、主流程跳转其余功能验证交给服务端接口用例。移动端的稳定性和速度都天然受限跑一条完整App用例通常要30秒到几分钟并行执行虽能缓解但设备成本和技术复杂度也随之上升。把这些成本集中投入到最高价值的主路径上是把有限的自动化预算花在刀刃上的一个标准做法。6.4 自动化测试平台能力别重复造轮子做自动化测试的人越来越多之后“自动化测试平台”这个概念也跟着火了起来。很多团队一开始是拿笔记录Excel用例然后想着一口气搭一个平台把用例管理、执行调度、报告展示、通知推送全部包揽。这个想法本身没错但我见过太多团队在“平台化”这件事上陷入自我消耗平台搭了三个月自动化用例一条没新增。我的建议是平台能力要尽量低成本复用优先用现成的工具链组合比如pytest管理用例、allure展示报告、Jenkins/GitLab CI做定时调度、钉钉/企业微信机器人发通知这些东西足够覆盖一个中型团队的自动化平台需求了。真正值得投入时间的是让这些工具链配合好而不是另起炉灶重新发明一个平台。7. 写在最后自动化测试的底线是“可信”我自己做自动化测试这么久最大的体会是自动化测试的价值从来不取决于用例数量也不取决于覆盖率报告多漂亮而是取决于团队是否信任它。一条用例如果三天两头误报失败团队成员会下意识地忽略它的结果那这条用例就失去了存在的意义甚至会反过来污染团队对测试的信心。所以这五个陷阱其实都不是“技术难点”而是“工程素养”的体现。定位器和等待讲的是你愿不愿意花心思写出稳定的代码测试数据讲的是你有没有边界意识断言讲的是你对自己验证的东西是否负责稳定性治理讲的是你面对偶发问题时是追根因还是糊弄过去测试分层讲的是你有没有把资源花在最重要的地方。这些问题看起来不起眼但它们决定了自动化测试能不能从“玩具脚本”走向“可信的工程质量保障”。最后分享一个我一直在用的小技巧每个迭代结束之后我都会打开allure的趋势报告不看总的通过率而是专门盯“上次通过、这次失败”的用例。这些用例是自动化测试里最值钱的信号它们往往意味着线上行为发生了预期之外的变化。逐个看下来比任何覆盖率工具都更能说明这个系统的真实健康状况。希望大家读完这篇文章能够避开我踩过的坑真正把自动化测试做成一件让团队放心的事情。
返回列表