做自动化测试这些年一直有人问我同一个问题Selenium到底值不值得深入学我的回答始终是值得。倒不是因为它在Web自动化工具里市场占有率高而是围绕它建立起来的那套核心能力——元素定位、等待策略、页面分层、数据与脚本分离——几乎可以无缝迁移到Appium、Playwright、Cypress这些后续工具上。这个系列写到今天基础环境和常用API已经聊得差不多了入门读者该踩的坑前面也都拆开讲过了。作为Selenium篇章的完结这一篇我不打算再罗列API我想把几个在一线项目里真正让我头疼、也真正拉开测试工程师差距的点一次性讲透。具体来说这篇会聚焦四件事复杂的自定义控件比如divulli组合出来的下拉框到底怎么定位和操作Page Object模式怎么进化出“仅存储定位元数据”的玩法一套能稳定跑进CI的Selenium框架骨架长什么样以及AI工具大规模介入后自动化测试脚本的生产方式正在发生什么变化。最后会聊聊在Web、移动端、图像识别工具之间如何做适合你自己的选型。这篇适合正在做UI自动化、准备重构维护困难脚本、以及想看看行业新方向的测试同学阅读。1. 逃不开的自定义下拉框divulli组合怎么稳定定位1.1 为什么业务系统偏爱自绘下拉框标准HTML里的select标签Selenium官方给了专门的Select类select_by_index、select_by_value、select_by_visible_text三种方式切换看起来非常省事。但真实业务系统里的前端早就被Vue、React这些框架接管像Element UI、Ant Design这类组件库默认渲染出来的“下拉框”DOM里根本找不到select标签清一色是div做容器、ul包列表、li做选项的结构。社区里“selenium定位获取下拉框元素不是原生下拉框”这个问题反复出现恰恰说明它是大部分人从入门到实战的第一道坎。为什么前端团队宁可自绘也不愿用原生select一是外观一致性问题原生select在不同操作系统和浏览器里渲染差异太大做不了精细的样式定制二是事件处理要挂在自定义结构上才灵活业务逻辑需要单选、多选、远程搜索、自定义模板这些能力原生控件很难满足。所以自绘下拉框不是某个团队的选择而是前端生态的整体趋势这就意味着做Web自动化必须直面它。顺带说一个判断技巧拿到一个下拉框先别急着写代码按F12看一眼DOM结构。如果看到select直接切Select类如果看到divulli就走下面这套自定义方案。很多时候脚本失败不是定位表达式写错而是第一步“确认控件类型”就搞错了。1.2 三套实战可用的定位操作方案第一套方案先点击触发区域等待下拉列表渲染完成再按可见文本匹配并点击目标项。这也是最通用、最符合用户真实操作的方式。以下是以Element UI的el-select为例的Python实现from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 1. 点击触发区域 trigger driver.find_element(By.CSS_SELECTOR, .el-select__wrapper) trigger.click() # 2. 等待下拉项全部可见 options WebDriverWait(driver, 10).until( EC.visibility_of_all_elements_located( (By.CSS_SELECTOR, .el-select-dropdown__item) ) ) # 3. 按文本点击目标项 for opt in options: if opt.text.strip() 自动化测试: opt.click() break第二套方案用XPath一步定位到目标项前提是下拉列表已经展开。XPath里用normalize-space()做文本匹配能有效避免列表项中多余空格、换行带来的误匹配driver.find_element(By.CSS_SELECTOR, .el-select__wrapper).click() target WebDriverWait(driver, 10).until( EC.element_to_be_clickable( (By.XPATH, //li[contains(class,el-select-dropdown__item) and normalize-space(.)自动化测试]) ) ) target.click()第三套方案如果目标元素明明在DOM里但点击被遮罩层拦截或者事件绑定在某些特定区域直接用JavaScript强制点击绕过可见性判断。这套方案是兜底手段不到万不得已不建议优先使用因为它跳过了用户真实操作路径可能掩盖遮挡类Bugelement driver.find_element(By.XPATH, //li[text()自动化测试]) driver.execute_script(arguments[0].scrollIntoView(true);, element) driver.execute_script(arguments[0].click();, element)1.3 下拉框操作里那几个共性的坑第一异步加载。很多下拉框的数据是接口返回后动态渲染的点击触发区域后列表不会立刻出现。如果直接find_element大概率抛NoSuchElementException。处理方式就是前面代码里先用WebDriverWait等元素可见再执行后续操作。第二动画过渡。下拉列表弹出时往往带transition动画元素在DOM里已经存在但位置还在移动。此时用visibility_of_element_located能过点击却报ElementClickInterceptedException。稳妥的做法是改成等element_to_be_clickable它在可见的同时还会判断元素是否可以接收点击。第三数据结构里藏的文本陷阱。有些下拉项前面带icon图标opt.text拿到的文本可能包含换行和多余空格有些长文本中间有span嵌套。用normalize-space(.)能同时处理这两种情况这也是XPath里我最常用的文本匹配函数。第四多选下拉和虚拟滚动。Element UI开启多选后每个选项上会出现复选框但点击逻辑没有区别直接点击li即可。真正麻烦的是带virtual-scroll的列表它只渲染可视区域内的DOMfind_elements拿不到全部选项。这时候就不能依赖一次性查找需要先滚动列表容器再分批获取或者干脆用搜索关键字的方式缩小选项集。2. 页面元素枚举与定位元数据分离把Page Object升级成数据驱动2.1 Page Object用在大型项目里会暴露什么问题Page Object模式本来是为了解决脚本可维护性问题一个页面建一个类元素作为成员变量业务动作封装成方法。小项目里这套思路非常舒服但项目跑到两三百个页面的时候问题就来了。首先是元素数量爆炸。后台管理系统的列表页、表单页动辄三四十个元素页面类文件越来越长光变量声明就占了大半篇幅真正有业务含义的方法反而被淹没。其次是定位表达式硬编码在类里前端一旦做样式重构比如把id改成># elements.yaml login_page: username: by: id value: username desc: 用户名输入框 password: by: css value: [namepassword] desc: 密码输入框 login_btn: by: xpath value: //button[contains(class,login-btn)] desc: 登录按钮再写一个ElementManager统一加载并解析这些元数据import yaml class ElementManager: def __init__(self, yaml_path): with open(yaml_path, encodingutf-8) as f: self._meta yaml.safe_load(f) def locate(self, page, element_name): 返回 (by_type, expression)供 find_element 使用 meta self._meta[page][element_name] return meta[by], meta[value] def all_elements(self, page): 枚举一个页面下的全部元素用于覆盖率统计 return list(self._meta[page].keys())使用的时候页面类不再关心具体定位方式class LoginPage: def __init__(self, driver, elements: ElementManager): self.driver driver self.elements elements def _locate(self, name): by, expr self.elements.locate(login_page, name) return self.driver.find_element(by, expr) def login(self, username, password): self._locate(username).send_keys(username) self._locate(password).send_keys(password) self._locate(login_btn).click()如果前端重构只需要改elements.yaml里对应元素的by和value测试代码一行都不用动。如果要统计覆盖率调用all_elements就能拿到某个页面下所有元素列表再和脚本运行时实际操作过的元素做比对自动化覆盖率就能量化了。2.3 这套玩法解决什么又带来什么副作用收益不只是改定位方式方便。把定位元数据单独抽出来后它变成了一份“团队共识文档”产品、开发、测试都能看懂页面有哪些核心交互元素新人接手项目翻开YAML就能快速了解系统结构比去代码里翻类快得多。而且这份元数据可以复用同一个元素在Web端用CSS定位在App端可能需要accessibility_id有了中间层之后多端复用成为可能。副作用也要说清楚。元素定位一旦全部配置化动态元素就会很尴尬——比如列表里每条数据的按钮id都带时间戳by和value根本无从写起。这类元素不适合放进静态元数据里应该保留在代码中做相对定位以父级稳定元素为锚点再向下找子元素。另外过度抽象会让定位链路变长报错信息里只有逻辑名、看不到实际页面结构排查问题时需要多跳一层。我的建议是核心高频页面元素用元数据驱动动态列表、特殊场景元素保留代码内定位不要把架构搞成纯数据流测试代码也是代码可读性永远优先。3. 从能跑脚本到能跑项目一套可进CI的Selenium框架骨架3.1 单脚本和框架之间到底差了什么“脚本能跑”和“项目能跑”是两件事。很多团队最初用Selenium都是写一堆独立脚本每个脚本自己启动浏览器、自己定位、自己断言跑的时候靠人肉盯着。结果就是测试失败了看不到清晰报告不知道是哪一步出问题没有失败重试机制网络偶发抖一下就报错用例之间共享登录状态一旦状态被污染后面一串全挂最致命的是没有和CI集成脚本只能手工触发。这种自动化跑起来比不跑还累最后往往变成“测试脚本给领导演示用的”。框架化解决的就是这些问题。一个合格的Selenium测试框架至少要覆盖这几个方面浏览器驱动的自动管理、测试用例的清晰组织和依赖隔离、断言失败后的现场保留、多维度的测试报告、失败用例自动重试、以及和Jenkins/GitLab CI的对接。3.2 基于pytest的最小可用框架Python技术栈里我推荐pytest selenium webdriver-manager allure这套组合。webdriver-manager解决了driver版本和浏览器不匹配的问题不用再手动下载chromedriver了。先看conftest.py里最关键的fixture和失败处理import pytest from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from datetime import datetime pytest.fixture(scopefunction) def driver(): service Service(ChromeDriverManager().install()) dr webdriver.Chrome(serviceservice) dr.implicitly_wait(5) dr.set_window_size(1920, 1080) yield dr dr.quit() pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: dr item.funcargs.get(driver, None) if dr: timestamp datetime.now().strftime(%Y%m%d_%H%M%S) dr.save_screenshot(f./report/screenshots/{item.name}_{timestamp}.png)driver这个fixture用scopefunction保证每个用例拿到独立的浏览器实例用例之间互不污染。失败钩子里保存截图截图路径以“用例名_时间戳”命名排错的时候一眼就能找到对应现场。这里有个经验隐式等待设5秒就好不要设太长不然等元素超时的时候一个用例可能要卡几十秒才失败。数据驱动用例的写法直接依赖pytest.mark.parametrizeimport pytest from pages.login_page import LoginPage pytest.mark.parametrize( username,password,expect_nickname, [ (test_user_01, pwd123456, 测试小张), (test_user_02, pwd654321, 测试小李), ] ) def test_login(driver, username, password, expect_nickname): page LoginPage(driver, element_manager) page.open(https://example.com/login) page.login(username, password) assert page.get_nickname() expect_nickname报告选Allure还是pytest-html我的看法是团队展示和排查用Allure更好它能把用例步骤、参数、截图、日志结构化展示如果只是内部回归CI跑一下pytest-html足够轻。别在这上面花太多时间报告工具随时能换。3.3 UI自动化与接口自动化如何各司其职很多团队的现状是“UI自动化跑全量回归”这是把测试金字塔整个倒过来了。UI自动化最擅长的是验证端到端的关键路径比如登录注册、下单支付、权限控制这些“从用户操作到系统反馈”的链路而业务数据的正确性、规则算法的边界应该交给接口自动化去覆盖。一个健康的分配方式大概是核心冒烟用例用UI自动化每天跑一遍接口自动化做全量业务回归UI层只做接口层无法覆盖的浏览器交互场景。UI自动化跑挂了先查是不是前端按钮位置变了接口自动化跑挂了查逻辑两类问题的归属清晰排查效率会高很多。当UI和接口都使用同一份测试数据体系和同一个报告平台时自动化体系才算真正成型。4. AI正在改写自动化脚本的生产方式4.1 Codex、Claude生成Selenium代码的真实水平这个话题必须聊因为AI对测试开发岗位的影响已经开始显现了。我的实测感受是用Claude这类大模型工具你把一段页面的HTML结构贴给它告诉它目标元素的特征它生成的XPath或CSS选择器大部分是可用的尤其是嵌套层级深、class命名规范清晰的页面AI的生成质量甚至比我手写还稳定。Codex更进一步你可以直接描述“这个用例登录之后没有断言昵称”让它去改已有代码它能自己定位问题区域并完成修改改完甚至能在本地执行一遍验证命令。但AI生成脚本有几个硬伤它不理解业务前置数据生成的用例往往缺少数据准备步骤对异步加载的等待策略要么给多要么给少直接生成的代码里很少看到合理的WebDriverWait它还容易生成“理想化”的定位表达式比如假设页面一定存在>基于以下元素元数据编写一条Selenium测试用例 - 用户名输入框css #username - 密码输入框css #password - 登录按钮xpath //button[text()登录] - 昵称文本css .navbar .nickname 业务场景用户输入正确的用户名和密码点击登录按钮期望跳转到首页 并在页面右上角看到用户昵称“测试小张”。 要求 1. 使用显式等待不要用sleep 2. 按 Page Object 结构组织代码 3. 断言使用 pytest 风格 4. 定位表达式必须从元数据中读取不允许硬编码AI给出的代码质量和提示词里的元数据精度强相关。元数据给得越完整包括等待条件、页面跳转关系、断言依据AI生成的用例就越接近可用状态。生成之后还是要人工review一遍重点看等待逻辑和异常路径处理。4.3 Cursor做移动自动化的能力边界社区里有人问“如何让cursor做手机自动化测试”这种问题暴露了一个普遍误解把AI编程工具当成能解决一切环境问题、设备问题的黑盒。真实情况是Cursor这类AI编程工具能帮你写Appium脚本、补全desired capabilities、生成Page Object但它替代不了移动自动化里最枯燥的环节——设备连接、系统日志抓取、App进程启动、定位权限弹窗处理、真机与模拟器的差异调试。这些是环境工程问题不是代码生成问题。移动自动化真正的瓶颈从来不是脚本怎么写而是设备管理和环境稳定性。AI可以帮你把Appium脚本写得更规范但DUT和远程设备池的连接、并发跑真机时的资源分配这些活儿AI现阶段帮不上任何忙。所以我的建议是先把Appium手动跑通一个Demo再把写脚本的活交给AI别搞反了顺序。4.4 面试风向AI时代的测试开发怎么聊最近“自动化测试面试题”里开始高频出现AI相关的问题面试官不太会问你“会不会用ChatGPT写代码”这种浅层问题更多是问你有没有用AI辅助写过自动化测试你怎么验证AI生成的代码是正确的比较好的回答思路是先讲清楚自己的工作流把页面结构喂给AI之前先自己确定元素定位锚点给AI设定输入输出约束要求它生成符合现有框架风格的代码生成代码后先静态审查再在目标环境里执行验证。最关键的一点是强调“AI负责生成、人工负责可信”——自动化测试的价值建立在结果可信之上AI生成代码之后必须由人来判断断言是否准确、覆盖率是否足够、失败是否可追溯。愿意往这个方向准备的人面试时明显会比只会背API的人更有竞争力。5. 选型与终局从Selenium出发但不止于Selenium5.1 Web、移动、图像识别三个工具世界怎么选Selenium的边界是Web浏览器自动化它解决的是“浏览器里能看到的交互”。移动端有Appium它本质上是WebDriver协议的移动生态扩展iOS和Android统一API但前提是你能搞定驱动和真机环境。SikuliX则是另一条路线它靠图像识别定位控件再模拟键鼠操作适合老旧的桌面程序、虚拟化环境、以及任何“元素不可见”的场景。三者的关系不是替代而是互补。下面这张对照表可以帮助做选型判断工具适用对象定位原理上手成本典型场景SeleniumWeb浏览器DOM元素定位低B/S管理系统、电商站点AppiumiOS/Android App原生/混合控件树中手机App功能回归SikuliX桌面应用、图像界面图像识别模拟键鼠低工业上位机、老系统、无人值守操作选型时先别问“哪个工具最强”先问三个问题被测对象是什么技术栈校验依据是界面展示、接口返回还是设备数据团队最熟练的语言是什么三个问题答完工具基本就定了。5.2 工业场景实例逆变器测试怎么做自动化选型热搜词里有个“逆变器自动化测试”我顺着这个场景说一下工具选型的实际逻辑。逆变器Inverter测试和纯互联网Web测试完全不同它通常有三层上位机测试软件负责监控和操作、被测设备DUT通过Modbus/CAN/串口等协议反馈电参数、测试系统负责记录和判定结果。这里被测对象的UI可能是C/S桌面程序也可能内嵌了B/S管理网页到底用哪个工具取决于产品形态如果上位机是浏览器访问的管理界面Selenium完全够用登录、点击、读数据显示值、截图留档这些动作都能覆盖。如果上位机是C#、LabVIEW写的桌面程序元素可访问性支持有限优先考虑WinAppDriver拿不到控件树的地方再用SikuliX做图像识别兜底。无论哪种UI光做到“界面点击”不算完整的自动化测试还必须写一个通信层去读设备实际返回的电压、电流、功率参数再和界面显示值做交叉比对。否则脚本通过设备实际数据错了都发现不了。这个例子说明一个道理自动化测试的工具选型永远要为被测对象的技术栈和校验依据服务脱离了具体场景谈“某工具最强”没有意义。Selenium只是其中一层真正值钱的是拆解场景的能力。5.3 一条从入门到进阶的可复制路径作为完结篇最后给一条我验证过很多次的路径供参考。阶段一把基本功练硬。元素定位、显式等待与隐式等待的区别、窗口句柄与iframe切换、浏览器事件模拟这些是Selenium的基石没有捷径只能在真实页面上反复练。阶段二完成从脚本到框架的跨越。把Page Object做出来再把元素定位抽成元数据接着接上pytest的报告和失败截图机制最后接进Jenkins或者GitLab CI让用例每天自动跑、报告自动出。阶段三拓宽工具视野。用Appium把一条移动端用例跑通用SikuliX处理一个桌面程序里的按钮点击理解不同工具之间的边界你就不再是“只会Selenium的人”而是“会做自动化测试的人”。阶段四拥抱AI但保持判断力。让AI帮你生成样板代码和定位表达式但每一段AI代码都要经过自己的逻辑审查和安全确认。工具会变但测试设计和判定逻辑永远不会过时。最后讲两句个人体会。做了这么多年自动化测试我的判断是测试开发这个岗位真正的护城河从来不是某个框架用得多熟而是在一个真实业务系统里你能不能让自动化测试稳定、可信、可维护。Selenium的语法可能过几年就被Playwright替代但你在定位、等待、分层、数据驱动、元数据管理这些东西上积累的判断力换了工具依然生效。这个系列就此完结我也不打算再开新篇了。如果后面我在AI辅助测试方向上有新的实践再回来和大家分享到时候我们聊的可能就不是某个具体API而是怎么让测试设计跟上软件形态的变化。