多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java+Selenium+TestNG+Maven UI自动化测试框架搭建与实战

Java+Selenium+TestNG+Maven UI自动化测试框架搭建与实战 搞自动化测试差不多绕不开这套组合IDEA Java Selenium TestNG Maven。就算你以前用的是别的语言做测试只要跳到Java这一侧最后大概率也会落到这套工具链上。网上相关的零散教程不少但很多要么只讲了安装要么只丢一段代码真正从环境搭建讲到框架设计、再讲到实际排坑的完整内容反而不多。这篇文章我想把自己在项目中实际用这套组合做UI自动化的经验完整梳理一遍从为什么选Maven和TestNG开始到工程结构、页面元素管理、复杂下拉框处理再到我踩过的几个典型问题一次性讲透。不管是刚接触自动化测试的新手还是已经写了一阵子脚本但感觉代码越来越难维护的同学这篇文章应该都能给你一些可以直接落地的思路。1. 为什么是这套组合从技术选型看测试框架的底层逻辑很多人在搭建自动化测试框架时习惯先去搜“哪个工具最好用”却忽略了先想清楚一个问题你的框架到底要解决什么。UI自动化的核心痛点从来都不是“能不能跑通一条脚本”而是“跑错的时候能不能快速定位”、“开发提测之后脚本还能不能稳定复用”。围绕这两个目标Java Selenium TestNG Maven这套组合几乎就是最成熟的答案。先说Maven。如果用最简单的方式写自动化你可以直接把Selenium的jar包下载下来塞到IDEA的Library里立即开写。这个方案在只有一个测试类、一个脚本文件的时候完全没有问题但一旦测试规模变大jar包依赖管理就会变成灾难。你的同事电脑上可能缺了某个版本的jarCI服务器上又是另一套版本跑起来的结果完全不可控。Maven最大的价值在于依赖的集中声明和传递性依赖的自动解析你在pom.xml里写上一个selenium-java的坐标它会把selenium本身依赖的一大堆库全部拉下来并且保证所有测试代码都使用同一份依赖。这解决了团队协作和持续集成环境一致性的根本问题。再说TestNG。很多人问为什么不直接用JUnit毕竟JUnit在单元测试里更普及。但TestNG对自动化测试场景的支持明显更强核心差异体现在几个地方。一是注解体系更加灵活BeforeMethod、AfterMethod这组注解天然适配“每个测试方法前重新打开浏览器”的UI测试习惯而BeforeClass可以让你在某些场景下复用同一个浏览器实例。二是参数化测试和DataProvider这两个能力在UI测试里特别实用同一个登录流程配合几组不同的账号密码数据一个测试方法就能跑完代码量大幅减少。三是更细粒度的分组执行和失败重试机制比如你可以通过Test(groups smoke)只跑冒烟用例出了偶发性的元素超时问题也可以通过IAnnotationTransformer做失败重试。这些功能在UI自动化这种很容易受环境影响的场景里价值非常直接。Selenium本身没什么可多说的它就是当前Web UI自动化的事实标准。别的工具比如Playwright、Cypress这几年也起来了各有优势但Selenium的生态积累和跨浏览器支持目前仍然是最稳的。尤其是Selenium 4之后封装了相对定位器和更好的等待机制相比旧版的体验提升明显。IDEA作为Java开发IDE就不用多讲了它的Maven支持、插件生态、调试体验都是我用过的IDE里最好的。1.1 工程结构怎么设计才不会越写越乱框架选型定了之后第二个关键决策是工程结构。很多测试代码写着写着就乱了根因是缺少统一的分层规范。我的建议是采用一个相对标准的Maven工程结构把不同职责的代码拆到不同的包和目录下selenium-testng-framework/ ├── pom.xml ├── testng.xml └── src ├── main │ └── java │ └── com.example.framework │ ├── core // 核心封装DriverFactory、WaitUtils、ScreenShotUtils │ ├── pages // Page Object一个页面一个类 │ └── tests // 测试用例类 └── test ├── java │ └── com.example.framework │ └── cases // TestNG用例 └── resources ├── testng-smoke.xml └── config.properties很多初学者会问为什么要多建一个src/test/java目录直接放在src/main/java下面不行吗这里其实是Maven的默认约定src/main/java放的是生产代码src/test/java放的是测试代码。测试用例放在test目录下有几个实际好处打包时生产包不会带上测试代码Maven执行生命周期时test阶段会默认扫描test目录CI流水线里跑自动化测试时可以直接用mvn test触发不需要额外配置。至于pages和tests这类包就是在执行Page Object模式时的组织方式。我特别想强调Page Object模式的价值。简单说就是把页面上所有元素定位和操作行为封装到一个页面类里测试用例只描述业务逻辑不直接接触定位器。比如你写一个登录测试用例里只写loginPage.inputUsername(admin)页面元素的id、name、xpath全部隐藏在LoginPage这个类里。这样做的直接收益是前端改版导致定位符变了的时只需要改页面对应类的代码所有使用该页面的用例都不受影响。真实项目里这种好处比想象中还要明显尤其是当你的用例数量超过几十条的时候。2. 保姆级环境搭建IDEA、Maven、TestNG的完整配置过程环境搭建这部分网上教程很多但不少都不够细致经常装到一半卡住。这里我会把从JDK到Maven到IDEA里运行第一段Selenium代码的完整流程写一遍。目标是你拿着这段文字按顺序操作一遍就能跑起来不用再回头去查别的资料。先说基础环境的版本选择。JDK建议直接用Java 8以上如果你现在新装可以考虑JDK 11或17。Selenium 4.x对Java 8仍然兼容但后续Selenium的新版本大概率会往更高的JDK版本走直接用个新一点的JDK可以避免后面升级依赖时还要换环境。IDEA社区版完全够用不用特意去找付费版的功能自动化测试涉及的Maven、Gradle、TestNG支持社区版全都包含。Maven的话我建议直接用3.8.x或者3.9.x版本不要用太老的3.5.x不然有些插件的兼容性会有问题。2.1 Maven下载、配置阿里云镜像与本地仓库Maven装起来不复杂但有一个点很容易被忽视国内直接访问Maven中央仓库速度非常慢经常依赖下载到一半就超时。解决办法是配置阿里云镜像。先去Maven官网下载对应版本的二进制包比如apache-maven-3.9.6-bin.zip然后解压到一个不含中文和空格的目录。接下来设置环境变量M2_HOME指向解压目录path里加上%M2_HOME%\bin。装完在命令行执行mvn -v能看到版本信息就说明配置成功了。然后去编辑Maven安装目录下的conf/settings.xml在mirrors节点里加入阿里云镜像mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror这样配置完之后所有依赖都会优先从阿里云镜像拉取下载速度会有非常大的改善。还有一件事建议顺手做掉就是指定本地仓库位置。默认情况下Maven会把依赖下载到用户目录下的.m2/repository里这个路径会占用C盘空间。你可以在settings.xml里修改localRepository标签指向你希望存放依赖的位置避免C盘被塞满。2.2 IDEA里创建Maven工程并集成TestNGIDEA配置Maven也很简单。打开IDEA进入Settings - Build, Execution, Deployment - Build Tools - Maven把Maven home path指向你本地的Maven安装目录再确认settings.xml用的是你配了镜像的那份配置文件。新建工程时选择Maven模板GroupId可以填你公司的逆向域名ArtifactId填项目名。创建完成之后打开项目里的pom.xml把依赖坐标写进去。这里我给你一份可以直接用的最小依赖清单dependencies !-- Selenium Java 客户端 -- dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.18.1/version /dependency !-- TestNG 测试框架 -- dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version7.9.0/version scopetest/scope /dependency !-- WebDriverManager自动管理浏览器驱动 -- dependency groupIdio.github.bonigarcia/groupId artifactIdwebdrivermanager/artifactId version5.7.0/version scopetest/scope /dependency /dependencies注意selenium-java这个依赖它会传递依赖引入Selenium的完整组件包括selenium-api、selenium-chrome-driver、selenium-support等。也就是说你不需要一个一个手动添加Selenium子模块的依赖。webdrivermanager这个库很多人一开始不熟悉它最大的作用是自动处理浏览器驱动的下载让本地不需要再手动配置ChromeDriver的路径对应的Selenium Manager在Selenium 4.11版本开始也内置了类似的能力但WebDriverManager使用上还是要顺手一些。TestNG本身还需要IDEA插件支持不然在IDEA里右键没有“Run As TestNG”的选项。在IDEA的插件市场里搜索TestNG安装之后重启就可以了社区版同样支持。这里注意一点TestNG插件装好之后第一次运行测试用例时要确认IDEA把pom.xml里的TestNG依赖识别成了测试库。如果右键没有TestNG的运行选项检查一下项目结构里是否有对应的依赖。2.3 用WebDriverManager写第一段可运行的脚本环境全部就绪之后最直接验证环境好坏的方式就是写一段最简单的脚本让浏览器打开一个页面。你可以新建一个测试类先放在test目录下写下面这段代码import io.github.bonigarcia.wdm.WebDriverManager; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; public class SmokeTest { public static void main(String[] args) { WebDriverManager.chromedriver().setup(); WebDriver driver new ChromeDriver(); driver.get(https://www.baidu.com); System.out.println(页面标题 driver.getTitle()); driver.quit(); } }如果环境配置正确运行main方法后会看到本地自动下载了匹配当前Chrome版本的driver然后弹出一个浏览器窗口并访问百度控制台输出页面标题最后窗口自动关闭。这里有一个常见问题值得提前说Chrome和ChromeDriver的版本必须兼容如果本地Chrome升级了但driver还是旧版本启动就会报SessionNotCreatedException错误。WebDriverManager的作用正是解决这个问题它会自动检测本机浏览器版本并下载对应的driver。这段代码虽然是能跑的但真正在测试框架里没有人会直接在测试类里new一个ChromeDriver就跑那样每条用例都得重新写一段driver初始化和关闭的逻辑。这个时候就需要做基础封装了。3. 测试框架核心代码实现Driver管理、BasePage与TestNG用例写到这里我们才真正进入框架搭建的核心部分。从这一节开始我会按照实际项目中比较成熟的写法把框架从底层到上层逐步实现出来。这里的代码不是展示片段而是可以直接用于你后续测试工程的模板。我会把每段代码背后的设计意图说明白这样你拿过去之后也敢根据具体情况调整。3.1 封装统一的Driver实例避免并发和重复启动问题UI自动化测试中driver对象的管理是最基础但最容易出问题的环节。你可能会遇到两类典型问题一类是多个测试方法串行执行时每个方法都新起一个driver上一个还没关干净下一个又启动了导致资源冲突另一类是多线程并发执行时多个线程共用同一个driver实例页面状态互相干扰。成熟的解决方案是用ThreadLocal保存driver实例。ThreadLocal可以理解为每个线程独享一份变量副本这样并行执行时每个测试线程都有自己独立的driver互不影响。在单线程执行时也不会因为多次初始化driver而导致浏览器窗口堆积。package com.example.framework.core; import io.github.bonigarcia.wdm.WebDriverManager; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; public class DriverFactory { private static final ThreadLocalWebDriver DRIVER_THREAD_LOCAL new ThreadLocal(); public static WebDriver getDriver() { if (DRIVER_THREAD_LOCAL.get() null) { DRIVER_THREAD_LOCAL.set(createDriver()); } return DRIVER_THREAD_LOCAL.get(); } public static void quitDriver() { WebDriver driver DRIVER_THREAD_LOCAL.get(); if (driver ! null) { driver.quit(); DRIVER_THREAD_LOCAL.remove(); } } private static WebDriver createDriver() { WebDriverManager.chromedriver().setup(); ChromeOptions options new ChromeOptions(); // 无头模式适合CI环境执行 // options.addArguments(--headlessnew); // 禁用浏览器自动化提示条和默认弹窗 options.addArguments(--disable-infobars); options.addArguments(--no-default-browser-check); options.setImplicitlyWait(5, java.util.concurrent.TimeUnit.SECONDS); return new ChromeDriver(options); } }注意implicitlyWait这个设置它设置了浏览器驱动在查找元素时最长等待5秒。不过这里我要特别提醒一下隐式等待和显式等待不要混用否则可能会产生意想不到的等待时间叠加问题。后面我们会重点讲显式等待的用法。3.2 BasePage把公共操作沉淀成一层的核心设计Page Object模式在上一节已经提到了但真正要让Page Object模式好用还需要一个BasePage基类。所有具体的页面类都继承这个BasePage这样公共方法只需要写一遍后面每个页面类都变得很薄只关心自身特有的元素和操作。BasePage里通常包含的内容有driver实例、显式等待的WebDriverWait工具、公共的元素操作方法、公共的截图方法等。这里给一个参考实现package com.example.framework.core; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import java.time.Duration; public class BasePage { protected WebDriver driver; protected WebDriverWait wait; public BasePage(WebDriver driver) { this.driver driver; this.wait new WebDriverWait(driver, Duration.ofSeconds(10)); } protected WebElement findElement(By by) { return wait.until(ExpectedConditions.presenceOfElementLocated(by)); } protected void click(By by) { WebElement element wait.until(ExpectedConditions.elementToBeClickable(by)); element.click(); } protected void input(By by, String text) { WebElement element findElement(by); element.clear(); element.sendKeys(text); } // 截图等方法按需扩展 }你可能会注意到我在click和findElement方法里都用了WebDriverWait。这是UI自动化里最常见也最重要的一个原则页面加载和元素渲染是不可控的。如果你直接执行driver.findElement(...).click()很可能因为元素还没渲染出来就报NoSuchElementException或者虽然元素存在但被遮罩层挡住无法点击报ElementClickInterceptedException。使用wait.until配合ExpectedConditions可以等元素达到可点击状态再操作能大幅降低脚本的脆弱性。3.3 写一个登录页的Page Object类让用例只关心业务有了BasePage之后你可以开始写具体的页面类了。以最经典的登录场景为例假设页面上有用户名输入框、密码输入框、登录按钮。页面类代码大致长这样package com.example.framework.pages; import com.example.framework.core.BasePage; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; public class LoginPage extends BasePage { private final By usernameInput By.id(username); private final By passwordInput By.id(password); private final By loginButton By.xpath(//button[contains(text(),登录)]); private final By errorTip By.className(error-tip); public LoginPage(WebDriver driver) { super(driver); } public void login(String username, String password) { input(usernameInput, username); input(passwordInput, password); click(loginButton); } public String getErrorTip() { return findElement(errorTip).getText(); } }这里把页面元素的定位信息统一放在页面类的字段位置一眼就能看到当前页面依赖哪些元素后续修改定位信息时也集中。实际项目中如果一个页面元素特别多你也可以用一个独立的定位类或者枚举来管理这个后续我们专门展开讲。总之Page Object模式的关键是页面细节都放在页面类里测试用例尽量不出现By.id这种底层代码。3.4 TestNG用例注解、断言和参数化的正确玩法有了页面类之后测试用例层需要承担的任务就很清晰了描述业务场景执行断言。这里用TestNG的常用写法展示一个登录成功和登录失败的用例package com.example.framework.cases; import com.example.framework.core.DriverFactory; import com.example.framework.pages.LoginPage; import org.openqa.selenium.WebDriver; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; public class LoginTest { private WebDriver driver; private LoginPage loginPage; BeforeMethod public void setUp() { driver DriverFactory.getDriver(); driver.get(http://localhost:8080/login); loginPage new LoginPage(driver); } AfterMethod public void tearDown() { DriverFactory.quitDriver(); } Test(description 正确账号密码可以成功登录) public void testLoginSuccess() { loginPage.login(admin, 123456); Assert.assertTrue(driver.getCurrentUrl().contains(/home), 登录成功后应跳转到home页面); } Test(description 错误账号密码提示登录失败) public void testLoginFail() { loginPage.login(admin, wrong); Assert.assertEquals(loginPage.getErrorTip(), 用户名或密码错误); } }TestCase的结构非常清晰setUp和tearDown负责driver的初始化和释放测试方法里几乎只有业务行为和断言。这两个方法对应TestNG的BeforeMethod和AfterMethod注解每个测试方法执行前都会运行setUp执行完都会运行tearDown这是用例隔离的标准做法。TestNG的断言功能也值得一提。Assert.assertEquals如果失败会抛出AssertionError并标记当前测试方法为失败TestNG会把这个信息记录到测试报告中。断言消息建议尽量写清楚比如我在assertTrue里补充的“登录成功后应跳转到home页面”一旦测试失败看一眼报告就能知道是哪一个环节出了问题不需要再去翻日志。参数化测试是TestNG很实用的一项能力。对于登录这种需要多组数据进行场景覆盖的用例可以用DataProvider替代写多个Test方法DataProvider(name loginData) public Object[][] loginData() { return new Object[][]{ {admin, 123456, true}, {admin, wrong, false}, {guest, 123456, false} }; } Test(dataProvider loginData) public void testLoginMulti(String username, String password, boolean expectSuccess) { loginPage.login(username, password); Assert.assertEquals(driver.getCurrentUrl().contains(/home), expectSuccess); }用DataProvider的好处是数据与测试逻辑分离将来新增一组测试数据不需要改动测试方法本体。如果你的数据源是从Excel或数据库读取的也只需要改DataProvider的实现方式测试代码完全不用动。到这里一个能跑的框架雏形已经有了。但要把这个框架用在真实项目里还有两个绕不开的进阶点需要仔细处理一是页面元素数量庞大时怎么管理定位信息更合理二是页面里存在非原生下拉框、弹窗、iframe等复杂组件时Selenium原生定位方式会失效得专门做适配。下面我针对这两个点展开细说。4. 进阶实践页面元素枚举化设计与复合型下拉框定位处理很多测试框架做到能跑、能出报告看起来功能都正常但一旦被测系统的页面数量上去就会立刻暴露出维护成本高的毛病。最典型的表现是页面上几十个元素的定位信息散落在各个页面类的字段里或者更糟直接在测试方法里到处写driver.findElement(By.id(...))改版时一个个找位置改到崩溃。这一节要讲的元素枚举管理方式就是为了解决这个问题。4.1 为什么单独写一套“枚举 存放定位元数据”的元素管理方案常规的Page Object模式把定位信息写在页面类里对于中小型项目已经够用。但如果你测试的是一个后台管理系统一个页面有四五十个元素你会发现页面类里的字段又长又多而且定位方式五花八门有的用id有的用name有的用xpath一眼望去很难维护。更重要的是一个元素可能被多个页面类复用一旦修改定位符副作用范围不好评估。更好的做法是把元素定位元数据独立出来用枚举统一管理。每个枚举项包含两个信息定位方式和定位表达式。比如package com.example.framework.pages; import org.openqa.selenium.By; public enum LoginPageElements { USERNAME(id, username), PASSWORD(id, password), LOGIN_BUTTON(xpath, //button[contains(text(),登录)]), ERROR_TIP(class, error-tip); private final String type; private final String value; LoginPageElements(String type, String value) { this.type type; this.value value; } public By toBy() { switch (type) { case id: return By.id(value); case xpath: return By.xpath(value); case class: return By.className(value); default: throw new IllegalArgumentException(未支持的定位方式: type); } } }使用时就变成下面这样public class LoginPage extends BasePage { public void login(String username, String password) { input(LoginPageElements.USERNAME.toBy(), username); input(LoginPageElements.PASSWORD.toBy(), password); click(LoginPageElements.LOGIN_BUTTON.toBy()); } }很多人在第一次看到这种写法时会觉得多此一举但实际用下来好处非常明确。首先所有定位信息集中在枚举里一个页面对应一个枚举类看枚举就能知道页面上有哪些元素以及用什么方式定位维护定位信息时不用到处切换文件。其次枚举天然带有语义化名称代码的可读性也提升了。再往后如果你希望做到“定位失败时自动输出可读错误信息”枚举项本身还可以附带文字描述字段比如LOGIN_BUTTON(xpath, //button[contains(text(),登录)], 登录按钮)排查问题时直接打印元素的中文名称比打印xpath直观多了。注意我这里说的仅是“存放定位元数据”的设计思路不是让枚举直接实现WebElement的获取逻辑。枚举的职责就是保存定位元数据并转换成By对象真正去等待、查找、操作元素的工作仍然交给BasePage里的方法。职责分离之后你可以在不改动测试逻辑的前提下调整任何一个元素的定位表达式这对频繁改版的前端项目来说意义很大。4.2 非原生下拉框div ul li 组合的定位与操作前端页面里原生select标签已经被淘汰了一大半现在主流的UI框架比如Element UI、Ant Design做出来的下拉框都是通过div、ul、li组合模拟的这种下拉框用Selenium原生的Select类处理不了因为Select类内部依赖原生select标签的属性。遇到这种情况得转换思路用“点击展开 等待选项渲染 定位目标选项并点击”的步骤来操作。我先给一个非常典型的DOM结构这样的结构在现在的后台管理系统里很常见div classcustom-select idcitySelect div classselect-trigger请选择城市/div ul classselect-dropdown styledisplay: none; li>public void selectCity(String cityName) { // 1. 点击触发下拉框展示 driver.findElement(By.cssSelector(#citySelect .select-trigger)).click(); // 2. 等待下拉列表渲染出来且可点击 WebElement dropdown new WebDriverWait(driver, Duration.ofSeconds(10)) .until(ExpectedConditions.visibilityOfElementLocated( By.cssSelector(#citySelect .select-dropdown))); // 3. 根据文本内容精确匹配目标选项并点击 WebElement option dropdown.findElement( By.xpath(.//li[contains(text(), cityName )])); option.click(); }有几个容易踩坑的细节需要特别说明。第一不要一上来就定位li并进行点击因为下拉框在点击触发之前通常是display:none状态直接查找元素会找不到或者找到但不可见。必须有一个显式等待确保下拉列表真正渲染出来。第二选项的定位最好基于下拉列表容器做相对查找也就是我在xpath前面加的那个点.//li表示在当前节点内部查找而不要用全文档范围的//li。如果页面里有多个下拉框全文档范围查找极容易匹配到错误元素。第三如果下拉选项是动态请求渲染的比如点击触发之后异步请求接口再渲染列表那么等待条件最好用visibilityOfElementLocated配合一个新的等待或者用ExpectedConditions.textToBePresentInElementLocated等文本出现避免列表还是空壳的时候就执行后续点击。处理这一类自定义组件的通用方法是先理解组件的交互模式再针对每个交互节点设置合理的等待条件。前端组件千变万化但“点击trigger - 等待popup出现 - 选择选项 - 等待popup消失”这个流程几乎覆盖了绝大多数下拉类组件。4.3 iframe内嵌元素与StaleElementReferenceException的处理除了复合型下拉框真实项目里还会遇到两个比较恶心的问题iframe内嵌页面和元素状态过期。iframe的处理逻辑其实不复杂关键在于要先切换driver的上下文。假设页面上有一个登录框是嵌在iframe里的直接定位用户名输入框肯定找不到。正确做法是先切进iframe操作完成后再切回来// 切换进入iframe driver.switchTo().frame(loginFrame); // 此时才能定位到iframe内部的元素 driver.findElement(By.id(username)).sendKeys(admin); // 操作完成之后切回默认主文档 driver.switchTo().defaultContent();需要注意的是如果iframe在元素定位前还没加载完成switchTo().frame()会报找不到frame的错误。所以切换前最好也用wait等待frame的存在new WebDriverWait(driver, Duration.ofSeconds(10)) .until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(loginFrame));StaleElementReferenceException则是另一个高频问题它表示某个WebElement对象在DOM中已经过期了。常见场景是页面局部刷新之后之前获取到的元素引用失效再对它执行click或getText就会抛这个异常。遇到这个问题通用解法是重新获取元素引用。如果你用的是BasePage里的findElement和click方法因为每次操作都会先走wait.until重新查找元素所以大部分情况下可以天然避免StaleElement问题。在更复杂的场景下也可以再封装一个“元素重试查询”的工具方法捕获StaleElementReferenceException后重新定位public WebElement refreshAndFind(By by) { try { return wait.until(ExpectedConditions.presenceOfElementLocated(by)); } catch (StaleElementReferenceException e) { return wait.until(ExpectedConditions.presenceOfElementLocated(by)); } }这一节看起来在讲具体问题其实核心想传递的是一种思路UI自动化遇到组件定位困难时先别急着换一种超级复杂的xpath硬碰而是多想想这个组件在前端的真实交互逻辑和渲染时机。Selenium只是工具你对页面DOM结构的理解深度才决定了脚本的稳定性。5. 常见问题排查与避坑技巧实测中反复踩过的坑最后这部分我想专门整理一下在真实场景里反复出现的问题和排查经验。很多问题不是代码写错了而是对工具或环境的理解有偏差导致排查耗时很久。我尽量用速查表加上现场排查过程的方式把这些坑说透。5.1 环境类问题浏览器版本、Driver版本和Maven依赖我见过最多的问题就是Selenium启动浏览器时报session相关异常比如下面这种org.openqa.selenium.SessionNotCreatedException: Could not start a new session. Response 400: session not created: This version of ChromeDriver only supports Chrome version 114这句话的意思非常直白当前ChromeDriver只支持Chrome 114但你电脑上的Chrome升到更高版本了。这类问题的根源是ChromeDriver和Chrome版本不匹配。解决办法就是把它们对齐。如果你使用的是WebDriverManager它通常会在启动时检测本地Chrome版本然后下载对应driver但如果你在代码里手动设置了System.setProperty(webdriver.chrome.driver, ...)就绕过了WebDriverManager的版本检测机制容易出问题。所以让WebDriverManager自动管理driver是更省心的选择。Maven依赖相关的问题同样常见。比如只添加了selenium-api而没添加selenium-java运行时会报方法找不到的NoSuchMethodError依赖下载失败时本地仓库里残留了半截文件也会导致构建失败。处理办法是确认pom.xml里依赖坐标完整然后执行mvn clean dependency:resolve重新拉取依赖。如果依赖已经损坏可以删除本地仓库中对应的目录再重新下载。还有一个IDEA层面的经典问题明明pom.xml里已经加了TestNG依赖但右键测试类找不到“Run TestNG Test”。这种通常是因为IDEA还没重新导入Maven依赖或者TestNG插件没装。在Maven工具窗口点一下Reload All Maven Projects按钮基本就能解决。5.2 元素定位类问题等待不够、iframe切换和动态属性元素定位不到或点击失败排查顺序很重要。我自己的排查路径一般是第一步确认页面是否加载完成。如果手动打开页面也要等几秒才能看到元素那Selenium必然也会等测试脚本里必须给出足够的显式等待时间。这里再次强调driver.findElement是立即返回的它不会等元素出现只有WebDriverWait配合ExpectedConditions才有等待语义。第二步确认元素是否在iframe里。如果元素在iframe内而你没有切换那么永远都定位不到。打开浏览器的开发者工具看Elements面板里目标元素是否被iframe包裹是的话就按前面说的switchTo处理。第三步确认定位表达式在当前DOM中是否唯一。用driver.findElements(...)打印一下数量和实际的HTML片段对比一下有没有多个匹配项或者是动态属性导致xpath表达式里面写死了改变的ID。动态属性是最近前端框架带出来的新问题。比如一个按钮的id在每次页面刷新后都会变比如btn_1691312399如果你在xpath里写死了这个id刷新一次就会失败。处理动态属性的思路是优先使用稳定的定位方式比如相对定位、文本定位、class定位或者CSS selector里的部分属性匹配。5.3 稳定性问题偶发失败怎么定位和兜底UI自动化最容易被人诟病的就是“早上能跑下午就红了本地能跑CI上就挂了”。偶发性失败在UI测试里几乎是不可避免的但我们能通过定位和分析来尽量减少它的影响。遇到偶发失败第一步不是急着加线程等待而是先看失败信息中报的是哪一行、哪一个元素、什么异常类型。如果是ElementClickInterceptedException多半是元素被弹窗或遮罩挡住了此时可以看是否有关不掉的弹层或者用Actions模拟鼠标点击来绕开遮挡。如果是超时异常多半是等待条件设置得不够可以考虑把隐式等待时间和显式等待时间合理错开。更高级一点的方案是失败重试机制。TestNG本身没有内置重试注解但可以通过实现IRetryAnalyzer来完成。下面是一个简单的重试实现package com.example.framework.core; import org.testng.IRetryAnalyzer; import org.testng.ITestResult; public class RetryAnalyzer implements IRetryAnalyzer { private int retryCount 0; private static final int MAX_RETRY_COUNT 1; Override public boolean retry(ITestResult result) { if (retryCount MAX_RETRY_COUNT) { retryCount; return true; } return false; } }然后在测试方法上标注Test(retryAnalyzer RetryAnalyzer.class)。这样即使偶发性的元素超时导致用例失败也会自动重跑一次对于那些因为网络抖动导致的临时失败重试机制效果非常明显。但这里也提醒一句重试只是兜底不能把所有稳定性问题都寄托在重试上。频繁的偶发失败仍然值得去深挖根因否则测试结果会失真。5.4 常见问题速查表问题现象可能原因解决方案SessionNotCreatedExceptionChrome与ChromeDriver版本不匹配使用WebDriverManager自动管理driver版本NoSuchElementException元素未被渲染或在iframe中检查等待条件检查是否被iframe包裹ElementClickInterceptedException元素被遮罩层或弹窗遮挡用显式等待并检查是否有弹窗或改用Actions点击StaleElementReferenceExceptionDOM刷新导致旧元素引用失效重新定位元素封装refreshAndFind隐式等待与显式等待时间异常叠加等待策略使用不当统一使用WebDriverWait避免混用两种等待Maven依赖下载失败中央仓库访问慢或本地仓库损坏配置阿里云镜像删除损坏依赖后重新resolve结尾一点实际的体会这套框架从环境搭建到工程实现我断断续续在真实项目里调整了很多轮。最后分享一点个人经验不要一上来就追求“框架感”把一个空架子搭得非常漂亮各种封装、各种设计模式都堆上去但真正落到业务页面时却发现连一个稳定的登录脚本都跑不通。更好的路径是先像第二节那样跑通一条最简单的脚本然后把公共能力driver管理、等待、BasePage一点点沉淀出来等用例量上来了再顺手解决元素管理、失败重试这些进阶问题。自动化框架是长出来的不是设计出来的。你遇到的实际问题越多对这套工具的理解就越深。如果你正在搭自己的框架希望这篇内容能帮你少走几步弯路。
返回列表