ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

UI自动化测试稳定实战:框架选型、元素定位与CI集成全解

UI自动化测试稳定实战:框架选型、元素定位与CI集成全解 写UI自动化测试的脚本不难难的是让它稳定跑三个月还不怎么花钱维护。但很多刚接触这个方向的人上来就找框架、写脚本结果用例跑起来绿油油一换环境就红一半最后整个项目组对自动化失去信心。今天我把这些年做UI自动化测试的完整思路和实操经验拆开聊一遍从要不要做、框架选型到元素定位、用例分层、失败排查一次性讲清楚适合刚从功能测试转自动化的小白也适合正在折腾脚本稳定性的测试开发。1. 动手之前先盘一盘UI自动化这笔账1.1 什么项目真正适合UI自动化我想先说一句可能不太中听的话不是所有项目都适合做UI自动化。UI自动化在测试金字塔里处于最顶层执行成本、维护成本、不稳定概率都比单元测试和接口测试高得多。它最适合的场景是那些核心流程稳定、版本迭代频繁、需要频繁做回归验证的项目。反过来看如果你的产品还在原型期UI每周都在动页面结构经常推倒重来那现在投入写UI自动化基本是白烧钱。你可以先把核心业务逻辑沉淀成接口自动化等UI稳定了再补一层界面级回归。我之前见过一个团队在产品改版最频繁的时候硬着头皮维护了80条UI用例结果每天光修脚本就花掉一个专职人力这成本完全不划算。判断要不要做可以从三个角度评估第一核心流程是否已经连续多个版本没有大改第二是否每个版本都需要花几个工时手工反复点同一个流程第三团队有没有CI环境能把用例定时跑起来。三个条件都占齐了才值得投入做UI自动化。1.2 投入产出比怎么算UI自动化最容易被忽略的问题就是算不清账。一个UI用例的编写成本从分析页面结构、写定位、处理等待、加入工程框架到最后稳定跑通通常要花半天到一天。如果这个用例覆盖的业务流程手工执行三分钟搞定那这钱花得没有意义。我一般会按这个逻辑去估算首先计算手工回归一次当前核心流程需要多少时间然后预估接下来一年会发多少个版本把“手工回归总时长”和“自动化脚本的编写维护总时长”放在一起对比。自动化脚本还存在一个优势就是可以晚上跑、早晨看结果这是手工测试做不到的。只要项目再存活大半年这套账基本都能回本关键是别把范围铺太开优先覆盖主流程、高风险模块、频繁回归区域。另外要把维护预算考虑进去。页面结构不会永远不变你需要预留每周一到两个人时的维护时间。如果一个版本的UI改版会导致超过十条用例大改那就说明自动化用例写得太贴近页面细节了解法不是硬扛而是调整设计把容易变化的部分收敛到Page类里用例本身只保留业务步骤。1.3 做之前先定三层防线很多人有个误解觉得做了UI自动化就可以把手工测试大部分干掉。实际操作下来你会发现UI自动化最适合当“最后一道防线”而不是主力防线。我比较推荐的分工方式是底层逻辑用单元测试覆盖业务接口用接口自动化覆盖UI层只跑冒烟和高优先级回归用例。UI自动化主要负责一条主流程从入口到结束能否走通比如注册登录、下单支付、设置保存这种端到端验证。它不像接口测试那样可以精准定位是哪个服务出了问题但它能验证页面控件交互、前端渲染、异步请求整合起来是否正常这是接口测试覆盖不到的。这个定位也决定了你的用例数量不应该太多。一个小团队维护120到200条UI用例已经算很大了再多就会开始互相牵扯、执行时间失控、失败分析困难。与其追求量不如把核心路径打磨得足够稳。2. 框架选型机器跑腿也得有趁手的工具2.1 主流框架对比与选择逻辑选框架这件事没有最好只有最合适。我按Web端、移动端、接口配合三层拆开说。先看Web端。Selenium是生态最大、踩坑资料最全的选择几乎所有浏览器都有对应Driver团队里随便一个测试开发都多多少少写过。它的缺点是API偏底层等待机制需要自己搭但用熟了完全够用。Playwright是这几年的新宠最吸引我的几点是自动等待机制、iframe处理简单、自带移动端模拟而且能直接把截图和视频作为测试报告附件排查问题非常方便。如果你所在的项目是绿色起步没有历史包袱我推荐直接从Playwright入手。移动端目前还是Appium为主。它沿用了WebDriver协议意味着你如果已经熟悉Selenium的写法转过来成本很低。缺点是环境配置烦琐Android和iOS的驱动依赖不少而且真机机型的适配问题会让你疯掉。使用Appium时记得优先用WebView元素定位少用坐标点击坐标是最后的手段。接口层配合方面pytest是绕不开的底座。它本身不是UI测试工具但作为测试框架非常称手fixture管理资源、参数化跑多组数据、断言失败自动截图、配合Allure出报告都能干净利落地实现。下面所有示例我都用Pythonpytest来写。2.2 pytest如何管理UI资源的生命周期用pytest做UI自动化核心是管理driver的启动和退出。我习惯把所有公共的fixture放在conftest.py里这样所有用例文件都能直接使用不用每个文件重复写。比如最基础的driver管理可以用类似下面这样的写法pytest.fixture(scopefunction) def driver(): options webdriver.ChromeOptions() options.add_argument(--headlessnew) driver webdriver.Chrome(optionsoptions) driver.implicitly_wait(5) driver.set_window_size(1920, 1080) yield driver driver.quit()这里有几处细节值得多说一句。scopefunction意味着每条用例前后各启动、退出一次浏览器。这样做的好处是每个用例都绝对干净不受到前面用例遗留状态的影响坏处是执行时间会拉长所以适合用例数不多的场景。如果你用例很多可以考虑scopemodule同一个文件里的用例共用一个浏览器但用例间的session隔离就要自己做比如每一条用例启动前先清理cookie、清空本地存储。headless模式适合CI环境跑但本地调试我不建议一开始就无头因为你看不到页面到底什么样。我经常犯的错就是无头模式下定位写对了有头模式跑起来反而因为页面渲染时机不同而失败。后来养成一个习惯本地先有头调试跑稳了再上无头参数。2.3 多浏览器兼容怎么落地如果你的产品需要兼容Chrome和Firefox靠人工在两三个浏览器里反复点太浪费了。pytest内置的fixture参数化可以解决这个问题。先定义一个配置文件把需要执行的浏览器类型传进去# conftest.py def pytest_addoption(parser): parser.addoption(--browser, actionstore, defaultchrome, choices[chrome, firefox]) pytest.fixture def driver(request): browser request.config.getoption(--browser) if browser chrome: driver webdriver.Chrome(...) elif browser firefox: driver webdriver.Firefox(...) yield driver driver.quit()然后将用例标记成多浏览器执行pytest.mark.parametrize(browser, [chrome, firefox], indirectTrue) def test_login(driver): ...实际跑的时候命令行直接指定--browserfirefox就能针对单个浏览器执行。这套方案的核心思路很简单把“在哪个浏览器跑”和“测试是什么”分离跑的时候再传参决定。3. 定位器策略UI自动化的命门3.1 元素定位的优先级排序UI自动化里翻车率最高的环节就是元素定位。我见过太多脚本挂在NoSuchElementException上而这个异常九成能靠选对定位策略避免。我自己的优先级排序是这样的首先用id这是最稳定的定位方式因为有前端规范约束一个页面里id通常是唯一的。没有id再用name、class等属性然后才用CSS Selector最后才考虑XPath。XPath虽然最强能通过文本、层级关系找元素但它最大的问题是脆弱——只要页面层级稍微变动表达式就废了。这里分享一个很容易懂的生活类比定位元素就像在人群里找人。如果你要找的人穿了一件独一无二的红衣服那你就按这个特征找如果整个会场所有人都穿一样的工服你只能靠“站在第二排最左边”这样的相对位置来认。唯一属性就是“红衣服”层级结构就是“排和列”前者稳后者一换座位就抓瞎。实际工作中还需要注意动态值。如果id是类似user_1679529291这样每次刷新都会变的值那这个id等于废了需要用[id^user_]这样的前缀匹配。小心动态生成的值是所有定位策略里的第一条铁律。3.2 等待策略别让你的脚本跑得比页面快脚本跑得比页面快这是UI自动化新手最容易忽视的坑。页面还没渲染完脚本就去点按钮结果要么点不到要么报错。等待策略大致分三种强制等待、隐式等待、显式等待。强制等待就是sleep(3)在脚本里硬等三秒。这个办法简单粗暴但坑也在这里界面快的时候白等三秒界面慢的时候三秒又不够。它只能作为临时的调试手段长留在正式用例里是定时炸弹。隐式等待是给webdriver对象设置一个全局等待时间只要查找元素时没有立刻找到就轮询等待。这个机制的短板在于它只管元素“存在”不管元素“可点”。一个元素如果还在灰化状态disabled隐式等待并不会帮任何忙照样报错。所以我强烈推荐优先用显式等待把事情说清楚你要等什么等到什么状态才继续。下面是我常用的等待封装from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_and_click(driver, locator, timeout10): element WebDriverWait(driver, timeout).until( EC.element_to_be_clickable(locator) ) element.click()这里的element_to_be_clickable会同时检查两个条件元素在DOM里存在、元素是可见且可交互的。用它来点击按钮要比直接找元素再click稳得多。等待的超时时间我一般设10秒如果页面卡顿比较严重的业务场景会放宽到15到20秒。但不要无脑设大因为每一条用例的失败检测都会等待这么长时间失败一多整体执行耗时就很吓人。如果页面本身有异步请求光等待元素可点还不够。更稳的做法是额外等待接口完成——你可以在Network里看到某个请求返回再去操作UI。这个做法在Selenium里实现起来有点复杂需要开启日志捕获但收益很大。等接口而不等界面能规避掉大部分因为渲染节奏不一致产生的偶发问题。3.3 动态元素和复杂结构的处理套路前端现在普遍用Vue、React这类数据驱动框架很多页面的DOM结构是动态生成的。最典型的情况是列表项一个列表有十行数据每一行的删除按钮id可能是delete_1、delete_2、delete_3。这种动态元素该怎么定位套路是不要直接定位每个具体元素而是先定位到列表容器的稳定部分再沿层级往下找。比如先通过XPath找到包含“第几行”的唯一文本节点然后从该节点往上找数量按钮。这个思路是把定位拆成两层稳定的容器可变的子项变化的部分尽量放在相对路径里。iframe是新手的另一个大坑。很多时候元素在DOM结构里清楚得很但Selenium就是找不到。先检查一下这个页面是不是嵌套了iframe如果是需要先switch_to.frame()切进去操作完再switch_to.default_content()切回来。这个动作如果漏掉后面所有定位都会失败。我有一个排查习惯元素找不到时第一步就查看当前页面有多少个iframe、当前焦点在哪个frame往往问题就水落石出。4. 从零搭建一套可复用的用例工程4.1 Page Object模式别把页面细节散落在用例里写UI自动化最怕什么怕的是页面一改版几十条用例跟着一起改。避免这个问题的最经典方案是Page Object模式POM。这个模式的设计思想很好理解把每个页面封装成一个类页面上元素的定位方式、操作方法都放在类里面用例代码只关心业务动作不关心页面细节。打个比方看菜单点菜的时候你只需要告诉服务员“来一份宫保鸡丁”不用知道后厨具体用什么火候、什么配料。用例就是顾客Page类就是前台菜单页面DOM就是后厨。后厨改配方了你换菜单说明就行顾客不用改需求。一个简单的LoginPage可以这么写class LoginPage: def __init__(self, driver): self.driver driver self.username_input (id, username) self.password_input (id, password) self.login_button (id, login_btn) def login(self, username, password): self.driver.find_element(*self.username_input).send_keys(username) self.driver.find_element(*self.password_input).send_keys(password) self.driver.find_element(*self.login_button).click()然后在用例里只写page LoginPage(driver) page.login(user, passwd) assert driver.current_url.endswith(/dashboard)如果登录按钮id从login_btn改成了login_submit你只改LoginPage里一行所有调用登录的用例都不受影响。页面变化和用例逻辑之间的解耦全靠这一层遮断。4.2 测试数据怎么准备测试数据是UI自动化中最麻烦的环节之一。数据不变用例就稳定数据一变用例就开始花式失败。我总结下来有三类方案按优先级排序。最脆弱的是直接在UI上创建数据比如注册一个用户然后去登录。这种方式完全依赖UI如果页面有问题数据就造不出来用例没法继续。最常用的是通过数据库或接口直接准备数据把前置条件在setup里完成用例本身只做验证。比如我要测登录就在数据库里先插入一条已知密码的用户记录然后UI层直接登录。这种方式的优点是快、稳但要求测试环境有对应权限。纯随机数据生成适合用在验证输入校验的场景比如用户名长度限制、特殊字符过滤。但要注意随机数据可能导致断言不同比如页面展示的是随机字符串断言时就要通过参数化去比对。我自己的习惯是所有测试数据都通过fixture统一准备让数据准备和业务测试解耦。用例只声明“我需要一个已登录用户”至于这个用户是数据库造的、接口造的还是UI注册的由fixture去决定。4.3 断言粒度别把自己的命根子绑死在UI细节上断言写得好不好直接决定你的用例稳定性。我很早之前吃过一个亏断言一个列表页的总数显示为共100条结果产品改版把文案改成了100 records一条本来通过的用例突然全挂了而且是因为文字展示变了完全不涉及逻辑问题。从那以后我对UI断言特别谨慎。我现在的原则是尽量断言业务结果少断言UI细节。登录成功可以断言跳转后的URL包含期望路径或者断言页面出现了只有登录用户才能看到的用户名信息。支付成功可以断言订单状态从“待支付”变成“已支付”而不是断言某个按钮颜色变了。当然有一些UI特性本身就是测试对象比如按钮在特定条件下是否可用、错误提示文案是否按预期出现这时候直接断言这些细节是合理的。关键是区分清楚你在做业务验证还是在做样式验证。一般来说业务验证的优先级远高于样式验证。4.4 失败证据链截图、日志、视频一个都不能少用例失败不可怕可怕的是失败了不知道怎么失败的。一条没有证据的失败用例就只是一张红牌你得花大量时间恢复现场、猜原因。所以我在项目落地时一定会搭建一套失败自动取证机制。最基础的是失败自动截图。用pytest的fixture可以直接实现pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs[driver] driver.save_screenshot(fartifacts/{item.name}_failure.png) with open(fartifacts/{item.name}.html, w, encodingutf-8) as f: f.write(driver.page_source)这里保存了两样东西截图和当前页面HTML。截图能看出视觉上的状态HTML能让你分析DOM结构两者结合基本能定位八成的失败原因。Playwright就更方便了直接内置了page.screenshot()和page.video()的支持把上下文存成视频失败之后整个操作过程都能回放。再往前一步把Allure报告接进来给每个用例挂上截图、日志、执行步骤。配合CI定时任务跑完之后直接打开报告看附件排查效率会提升一个量级。4.5 CI集成无人值守跑用例才算自动化UI自动化跟CI结合才算真正发挥价值。我是把用例定时任务放在每天晚上第二天早上团队上班前执行然后把结果报告自动推到团队的沟通群里。如果有失败优先看失败用例的截图和堆栈五分钟内就能定位是环境问题、数据问题还是代码问题。执行策略上我建议分两套计划一套是冒烟测试每次代码合并到主干前手动触发或合并时自动跑十分钟到二十分钟跑完核心流程另一套是完整回归每天晚上跑全部用例用于隔夜验证。两套计划共用同一个测试工程只是通过pytest的-m标记筛选不同的用例组。# 冒烟 pytest -m smoke --browserchrome --maxfail5 # 全量回归 pytest -m regression --browserchrome --browserfirefox --alluredirallure-resultsmaxfail这个参数很关键它控制在遇到第几个失败时停止。通常我会设成5否则环境一旦出问题几十条用例排队失败白白浪费大量执行时间。5. 实战问题排查手册5.1 偶发失败先怀疑时序再怀疑定位偶发失败是UI自动化最大的敌人也是最磨人心态的。一条用例有时候过有时候不过这时候很多人第一反应就是去换定位表达式但其实大部分偶发问题不是定位问题而是时序问题。我总结的排查顺序是先看失败截图页面停在哪一步再分析这一步之前执行了什么动作、有没有异步请求没完成。通常的规律是页面里某个区域是接口返回后动态渲染的脚本点击时接口还没回来于是界面处于中间状态。解决方案是把隐性等待拉长或者增加显式等待条件针对特定元素等到期望状态。如果时序加了还是偶发那就要从数据层面怀疑了这条用例跑的时候前置数据是否存在是不是与其他用例共享了数据导致被清理或变更。我经常在数据集里发现一条用例把另一条用例需要的数据删掉了结果两条用例单独跑都稳定一起跑就一死一活。5.2 环境差异为什么你本地过了CI挂了“我本地明明过了CI里就挂”这句话可能是测试工程师每天说得最多的一句话。环境差异亘古存在无法消除只能尽量控制变量。首先是网络差异。CI机器的网络带宽和延迟跟本地不一样尤其是页面引用了大量外部资源时加载时间会差很多倍。应对方案是核心的显式等待尽量放宽并且把页面加载策略设置好比如忽略不必要的资源加载。其次是浏览器差异。CI通常跑headlessheadless模式下字体渲染、窗口尺寸、滚动条行为都和真实浏览器不完全一致页面里某些元素的位置、尺寸甚至可见性都会有细微差别。应对方案是统一设置窗口大小不要依赖某个具体分辨率下的坐标。再次是系统差异。Windows和Linux下的字体不同可能导致文本换行位置不同、元素高度变化布局一变化某些依赖相对位置的定位就会失效。这就需要尽量少用绝对坐标定位多用元素相对关系。5.3 维护成本失控的三类信号任何一套自动化系统都会面临维护成本上升的问题但它通常是缓慢累积的不回头复盘根本感觉不到。我建议定期关注三个信号用例总量是不是只增不减失败率是不是长期超过3%到5%每次版本改动后需要修改的用例是不是集中在同一批页面。三条里占了两条就说明你的自动化资产开始“沉淀坏账”了。处理办法是主动做自动化用例的“年度大扫除”。每过一两个季度我会全线跑一遍用例不看执行结果而是逐条审查用例本身还有没有人在关注它的运行结果如果一条用例连续一个月没有失败过同时覆盖的业务也没有变化它很可能已经沦为僵尸用例删掉也不可惜。与其维护一百条沉睡的用例不如留下三十条每次都真正有价值的核心用例这可能是UI自动化项目维护中最容易被忽视但最重要的一课。5.4 常见的Timeout类错误速查报错信息常见原因处理建议NoSuchElementException定位表达式过期元素被动态渲染或iframe遮挡优先检查iframe其次用相对定位或contains模糊匹配ElementClickInterceptedException元素被弹窗、浮层、遮罩遮挡先关闭弹窗或改用ActionChains点击ElementNotInteractableException元素存在但不可交互常因页面还在加载动画增加显式等待等待元素可点击StaleElementReferenceException页面刷新后之前保存的元素引用失效重新获取元素避免长时间保存元素引用TimeoutException等待条件在超时时间内未满足检查前置接口是否正常数据是否到位等待条件是否设对这一张表是我日常排查问题的默认清单绝大多数脚本稳定性的问题都能在里面找到对应项。真到了排查不出来的地步也不要硬撑视频回放加接口日志基本能把每一帧交互过程还原出来。6. 最后想说的几句话做UI自动化这几年我最大的感受是它拼的其实不是写脚本的能力而是对稳定性和可维护性的理解。只有用例稳定执行、失败能快速定位、维护成本被控制住自动化才能在团队里真正被信赖否则很容易沦为大家口中的“花架子”。如果你刚起步建议从一条主流程用例开始练手先跑通再谈铺量。过程中一定要养成记录问题的习惯尤其是那些偶发失败每解决一个就沉淀进自己的排查清单。日积月累你会发现很多看起来玄乎的问题在经验面前其实都有迹可循。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表