从零搭建轻量级Web UI自动化测试框架:Selenium+TestNG+POM实战指南

发布时间:2026/6/29 4:43:54
从零搭建轻量级Web UI自动化测试框架:Selenium+TestNG+POM实战指南 1. 项目概述为什么我们需要一个“简单”的框架做Web UI自动化测试的同行估计都经历过这个阶段一开始我们可能直接用Selenium WebDriver写几个测试脚本觉得挺方便。但随着项目迭代页面元素越来越多测试用例数量从几十个膨胀到几百个维护成本就开始指数级上升。今天A页面改了个按钮的ID明天B流程加了个弹窗你就得满世界找脚本去改定位器改完还得祈祷别引入新的错误。这种“脚本堆砌”的模式很快就会让自动化测试变得脆弱不堪最终沦为摆设或者成为测试团队的沉重负担。所以我们需要一个框架。但市面上成熟的框架像TestNG、Cucumber、Serenity BDD功能强大学习曲线也陡峭。对于很多中小型项目或者刚起步的团队来说引入这些“重型武器”可能有点杀鸡用牛刀配置复杂团队成员上手也慢。这时候一个“简单”但“够用”的自研框架价值就凸显出来了。我说的“简单”不是功能简陋而是指结构清晰、易于理解、维护成本低。它应该像一个工具箱把Selenium的原始能力封装成更易用的模块比如页面对象管理、测试数据驱动、测试报告生成。你不需要从零开始造轮子而是基于一些成熟的开源库Selenium WebDriver, TestNG, Log4j2, Apache POI等用Java把它们有机地组合起来形成一套适合自己团队和项目的约定和规范。这个框架的核心目标就三个提升脚本稳定性、降低维护成本、让非开发出身的测试人员也能快速上手编写用例。接下来我就结合自己搭建这类框架的经验拆解一下从设计思路到具体实现的完整过程并提供可以直接“抄作业”的代码模块和配置。2. 框架整体设计与核心思路拆解2.1 设计原则什么才是“好”的自动化框架在动手写代码之前我们先得想清楚一个好的、简单的UI自动化框架应该遵循哪些原则。我总结了四点这也是我们后续所有技术选型和架构设计的基石分离与封装这是最重要的原则。要把测试脚本业务逻辑、页面元素定位与操作、测试数据、环境配置彻底分开。脚本里不应该出现driver.findElement(By.id(“submit”)).click()这样的代码而应该是loginPage.enterUsername(“admin”).clickSubmit()。这样前端页面改了我们只需要去维护对应的“页面对象类”脚本基本不用动。数据驱动测试用例的逻辑和测试数据要分离。同一个登录流程应该能用多组不同的用户名/密码正确、错误、空值来执行。数据最好放在外部文件如Excel、JSON、YAML里方便非技术人员维护和添加用例。健壮性与可维护性框架要能优雅地处理各种异常比如元素加载超时、网络不稳定。要有完善的日志记录出问题时能快速定位。代码结构要清晰命名要规范方便后续其他人阅读和修改。易于集成与执行要能方便地集成到CI/CD流水线如Jenkins中支持命令行执行、分组执行、并行执行。测试报告要直观能清晰地展示成功、失败以及失败的原因和截图。2.2 技术栈选型与理由基于以上原则我们来选择具体的技术组件。这是一个典型的、经过大量项目验证的轻量级组合核心驱动Selenium WebDriver (4.x)为什么选它行业标准无需多言。它提供了直接操作浏览器的API支持所有主流浏览器Chrome, Firefox, Edge等。我们用它来实现最底层的页面交互。测试运行与管理TestNG为什么选它而不是JUnitTestNG在设计之初就考虑了更复杂的测试场景。它支持强大的测试分组groups、依赖测试dependsOnMethods、参数化测试DataProvider并且原生支持并行执行。这些特性对于管理成百上千的UI自动化用例来说非常实用。它的BeforeSuite/BeforeTest/BeforeMethod等注解能让我们更灵活地控制测试生命周期如全局初始化、测试类初始化、测试方法初始化。对象定位与等待Selenium 显式等待关键点绝对不要依赖Thread.sleep()我们要使用Selenium的WebDriverWait配合ExpectedConditions来实现智能等待。这是保证脚本稳定性的关键。我们会把这部分封装成一个公共的“等待工具类”。页面对象模式Page Object Model, POM这不是一个库而是一种设计模式。我们将每个Web页面或页面片段如Header, Footer抽象成一个Java类。这个类包含页面元素定位器使用FindBy注解声明。页面操作方法如clickLoginButton(),inputSearchKeyword(String keyword)。这样测试脚本就变成了对这些页面对象方法的调用清晰且易于维护。数据驱动Apache POI TestNG DataProvider为什么用Excel对于很多测试团队Excel是管理测试数据最熟悉的工具。产品经理、业务测试人员都可以直接编辑。Apache POI库可以很好地读写Excel文件.xlsx格式。我们将用DataProvider方法从Excel中读取数据并喂给测试方法。日志记录Log4j2为什么不用System.out.println专业的日志框架可以分级别DEBUG, INFO, WARN, ERROR输出可以控制输出到控制台、文件甚至数据库。在调试复杂的UI交互问题时详细的日志是救命稻草。Log4j2性能比Log4j好我们选它。报告生成ExtentReports 或 AllureExtentReports轻量级API简单可以生成非常美观的HTML报告并支持截图附件。Allure功能更强大可以与CI工具深度集成生成趋势图等。但对于“简单框架”的初衷ExtentReports可能更合适。本文将以ExtentReports为例。构建工具Maven管理项目依赖pom.xml实在太方便了。所有上述库的版本都可以通过Maven统一管理。2.3 项目目录结构规划一个清晰的项目结构是框架可维护性的基础。我推荐如下结构src/test/java/ ├── com.yourcompany.framework │ ├── base/ # 框架基础类 │ │ ├── BaseTest.java # 所有测试类的父类初始化Driver管理报告 │ │ └── TestBase.java # (可选) 更细化的基础功能 │ ├── utils/ # 工具类 │ │ ├── DriverManager.java # 单例模式管理WebDriver实例 │ │ ├── WaitUtil.java # 封装的显式等待工具 │ │ ├── ConfigReader.java # 读取配置文件(properties/yaml) │ │ ├── ExcelDataProvider.java # 读取Excel数据 │ │ └── ScreenshotUtil.java # 截图工具 │ └── listeners/ # 监听器 │ └── TestListener.java # 实现TestNG的ITestListener用于报告和截图 ├── com.yourcompany.pages # 页面对象类包 │ ├── LoginPage.java │ ├── HomePage.java │ └── common/ # 公共组件如Header, Sidebar │ └── HeaderComponent.java └── com.yourcompany.tests # 测试脚本包 ├── LoginTest.java └── SearchTest.java src/test/resources/ ├── config.properties # 配置文件浏览器类型、URL、超时时间等 ├── testdata/ # 测试数据文件 │ └── TestData.xlsx ├── drivers/ # 浏览器驱动也可通过WebDriverManager自动管理 │ ├── chromedriver.exe │ └── geckodriver.exe └── log4j2.xml # 日志配置文件这个结构把框架代码、页面对象、测试用例、资源配置分得清清楚楚任何新人拿到项目都能很快找到该修改的地方。3. 核心模块实现详解3.1 基石WebDriver的管理与封装WebDriver实例是UI自动化的核心资源。管理不当比如多个测试方法同时操作同一个Driver实例或者测试结束后没关闭导致进程残留会引发各种诡异问题。我们的目标是每个独立的测试方法都在自己全新的浏览器环境中执行测试结束后自动清理。实现方案使用ThreadLocal和单例模式我们创建一个DriverManager类。package com.yourcompany.framework.utils; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.firefox.FirefoxDriver; import io.github.bonigarcia.wdm.WebDriverManager; import java.util.concurrent.TimeUnit; public class DriverManager { // 使用ThreadLocal确保每个线程有自己的Driver实例支持并行测试 private static ThreadLocalWebDriver driverThreadLocal new ThreadLocal(); private DriverManager() {} // 私有构造防止实例化 /** * 获取当前线程的WebDriver实例如果不存在则创建 */ public static WebDriver getDriver() { if (driverThreadLocal.get() null) { initializeDriver(); } return driverThreadLocal.get(); } /** * 初始化WebDriver根据配置创建对应的浏览器实例 */ private static void initializeDriver() { WebDriver driver; String browserType ConfigReader.getProperty(browser); // 从配置文件读取如chrome // 使用WebDriverManager自动下载和管理驱动避免手动放置驱动文件的麻烦 switch (browserType.toLowerCase()) { case firefox: WebDriverManager.firefoxdriver().setup(); driver new FirefoxDriver(); break; case chrome: default: WebDriverManager.chromedriver().setup(); driver new ChromeDriver(); } // 全局等待策略隐式等待慎用主要用于找不到元素时的兜底 driver.manage().timeouts().implicitlyWait( Long.parseLong(ConfigReader.getProperty(implicit.wait)), TimeUnit.SECONDS ); // 页面加载超时 driver.manage().timeouts().pageLoadTimeout( Long.parseLong(ConfigReader.getProperty(page.load.timeout)), TimeUnit.SECONDS ); // 最大化窗口根据需求可选 driver.manage().window().maximize(); driverThreadLocal.set(driver); } /** * 关闭当前线程的Driver并移除ThreadLocal引用 */ public static void quitDriver() { WebDriver driver driverThreadLocal.get(); if (driver ! null) { driver.quit(); driverThreadLocal.remove(); // 关键必须移除防止内存泄漏 } } }实操心得与避坑指南为什么用ThreadLocalUI自动化测试尤其是结合TestNG的parallel属性进行并行测试时如果多个测试线程共享一个WebDriver实例会导致操作混乱测试结果不可预测。ThreadLocal为每个线程提供了独立的变量副本完美解决了并行问题。WebDriverManager是个神器它简化了浏览器驱动的管理。你不需要手动下载chromedriver.exe并放到系统路径或项目里。它会自动检测你本地安装的浏览器版本并下载匹配的驱动。只需在pom.xml中添加依赖即可。quit()vsclose()测试结束时一定要调用driver.quit()。它会关闭所有关联的窗口并终止浏览器进程。而driver.close()只关闭当前窗口如果只有一个窗口效果类似但为了彻底清理养成用quit()的习惯。隐式等待是双刃剑我设置了隐式等待但强烈建议在具体的页面操作中使用更可靠的显式等待。隐式等待是全局的可能会在某些场景下如等待元素消失导致不必要的长时间等待影响测试效率。它更像一个安全网。3.2 灵魂页面对象模式POM的优雅实现POM模式是实现“分离”原则的关键。我们利用Selenium提供的PageFactory类来简化页面对象的初始化。一个标准的页面对象类示例LoginPage.javapackage com.yourcompany.pages; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.PageFactory; import com.yourcompany.framework.utils.DriverManager; import com.yourcompany.framework.utils.WaitUtil; public class LoginPage { // 使用FindBy注解声明页面元素支持ID, Name, XPath, CSS等多种定位方式 FindBy(id username) private WebElement usernameInput; FindBy(id password) private WebElement passwordInput; FindBy(css button[typesubmit]) private WebElement loginButton; FindBy(className error-message) private WebElement errorMessage; // 构造函数用PageFactory初始化元素 public LoginPage() { // PageFactory.initElements 会将当前类中所有FindBy注解的WebElement进行“延迟绑定” // 只有当调用这些元素时Selenium才会去页面上查找 PageFactory.initElements(DriverManager.getDriver(), this); } // 页面操作方法输入用户名 public LoginPage enterUsername(String username) { WaitUtil.waitForElementVisible(usernameInput).sendKeys(username); return this; // 返回当前页面对象支持链式调用 } // 页面操作方法输入密码 public LoginPage enterPassword(String password) { WaitUtil.waitForElementVisible(passwordInput).sendKeys(password); return this; } // 页面操作方法点击登录 public HomePage clickLogin() { WaitUtil.waitForElementClickable(loginButton).click(); return new HomePage(); // 通常点击后跳转到新页面返回新页面的对象 } // 组合业务方法执行登录流程 public HomePage login(String username, String password) { enterUsername(username); enterPassword(password); return clickLogin(); } // 获取错误信息用于断言 public String getErrorMessage() { return WaitUtil.waitForElementVisible(errorMessage).getText(); } // 跳转到登录页 public void navigateToLoginPage() { DriverManager.getDriver().get(ConfigReader.getProperty(base.url) /login); } }配套的等待工具类WaitUtil.java这是保证POM健壮性的核心。package com.yourcompany.framework.utils; import org.openqa.selenium.*; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import java.time.Duration; public class WaitUtil { private static final long EXPLICIT_WAIT Long.parseLong(ConfigReader.getProperty(explicit.wait)); public static WebElement waitForElementVisible(WebElement element) { return new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(EXPLICIT_WAIT)) .until(ExpectedConditions.visibilityOf(element)); } public static WebElement waitForElementClickable(WebElement element) { return new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(EXPLICIT_WAIT)) .until(ExpectedConditions.elementToBeClickable(element)); } public static Boolean waitForElementInvisible(By locator) { return new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(EXPLICIT_WAIT)) .until(ExpectedConditions.invisibilityOfElementLocated(locator)); } // 可以继续添加其他常用的等待条件... }注意事项与高级技巧链式调用像enterUsername().enterPassword().clickLogin()这样的写法让测试脚本更简洁。这要求你的页面操作方法返回this或下一个页面的对象。不要缓存动态元素如果一个元素在页面上会频繁刷新或变化例如AJAX加载的列表不要在类变量里缓存它。应该在每次需要时在方法内部用FindBy重新查找或者直接使用driver.findElement。处理StaleElementReferenceException这是UI自动化中最常见的异常之一。当一个元素被找到后页面刷新或AJAX更新了DOM之前找到的WebElement对象就“过期”了。最佳实践是采用“即时查找”策略并封装重试机制。我们的WaitUtil在每次操作前都重新等待元素本身就部分缓解了这个问题。对于更复杂的情况可以写一个带重试的click方法。PageFactory的陷阱PageFactory.initElements默认使用“懒加载”和“缓存”。这通常是好的但在某些极端动态页面下可能有问题。如果遇到可以考虑在FindBy注解上加上cacheLookup false或者在操作方法内部直接使用DriverManager.getDriver().findElement(...)。3.3 血肉数据驱动测试的实现数据驱动让我们的测试脚本逻辑固定数据变化。我们结合TestNG的DataProvider和Apache POI来实现。Excel数据读取工具类ExcelDataProvider.javapackage com.yourcompany.framework.utils; import org.apache.poi.ss.usermodel.*; import org.testng.annotations.DataProvider; import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; public class ExcelDataProvider { DataProvider(name excelData) public static Object[][] getDataFromExcel(Method method) throws IOException { // 通常约定Excel文件名对应测试类名Sheet名对应测试方法名 String className method.getDeclaringClass().getSimpleName(); String methodName method.getName(); String filePath src/test/resources/testdata/ className .xlsx; FileInputStream fis new FileInputStream(filePath); Workbook workbook WorkbookFactory.create(fis); Sheet sheet workbook.getSheet(methodName); // 根据方法名获取Sheet int rowCount sheet.getLastRowNum(); // 最后一行索引0-based int colCount sheet.getRow(0).getLastCellNum(); // 第一行列数 ListObject[] dataList new ArrayList(); // 从第1行开始读第0行通常是表头 for (int i 1; i rowCount; i) { Row row sheet.getRow(i); if (row null) continue; Object[] rowData new Object[colCount]; for (int j 0; j colCount; j) { Cell cell row.getCell(j, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); rowData[j] getCellValue(cell); } dataList.add(rowData); } workbook.close(); fis.close(); // 转换为TestNG DataProvider需要的二维数组 return dataList.toArray(new Object[0][0]); } private static Object getCellValue(Cell cell) { switch (cell.getCellType()) { case STRING: return cell.getStringCellValue(); case NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { return cell.getDateCellValue(); } else { return cell.getNumericCellValue(); } case BOOLEAN: return cell.getBooleanCellValue(); case FORMULA: return cell.getCellFormula(); case BLANK: return ; default: return ; } } }在测试类中使用数据驱动LoginTest.javapackage com.yourcompany.tests; import com.yourcompany.framework.base.BaseTest; import com.yourcompany.pages.LoginPage; import com.yourcompany.pages.HomePage; import com.yourcompany.framework.utils.ExcelDataProvider; import org.testng.annotations.Test; import static org.testng.Assert.assertTrue; public class LoginTest extends BaseTest { // 继承BaseTest获得Driver和报告支持 Test(dataProvider excelData, dataProviderClass ExcelDataProvider.class, groups {smoke}) public void testLoginWithDifferentUsers(String username, String password, String expectedResult) { LoginPage loginPage new LoginPage(); loginPage.navigateToLoginPage(); if (SUCCESS.equals(expectedResult)) { HomePage homePage loginPage.login(username, password); // 断言登录成功例如检查首页是否显示了用户名 assertTrue(homePage.isUserLoggedIn(username), 登录成功后用户信息显示不正确); } else if (FAILURE.equals(expectedResult)) { loginPage.enterUsername(username).enterPassword(password).clickLogin(); // 断言登录失败检查错误信息 String actualError loginPage.getErrorMessage(); assertTrue(actualError.contains(无效的用户名或密码), 预期的错误信息未出现); } } }对应的Excel文件LoginTest.xlsx里面有一个名为testLoginWithDifferentUsers的Sheet数据如下usernamepasswordexpectedResultadmincorrect_passwordSUCCESSadminwrong_passwordFAILURElocked_userany_passwordFAILURE(空)(空)FAILURE数据驱动设计的经验数据与脚本解耦测试工程师甚至产品经理可以直接维护Excel无需触碰Java代码。灵活的数据提供器上面的例子是“一方法一Sheet”。你也可以设计成“一类一Sheet”通过方法名来过滤数据行更灵活。复杂数据的处理如果测试数据是复杂的对象比如一个完整的订单信息可以考虑用JSON或YAML文件然后反序列化成Java对象。DataProvider返回的可以是Object[][]也可以是IteratorObject[]。注意数据类型从Excel读取的数字可能会是Double类型如果测试方法参数是String需要做好转换或者在工具类里统一处理成字符串。3.4 仪表盘测试报告与日志集成测试跑完了我们需要一个清晰的结果反馈。这里我们集成ExtentReports。首先在BaseTest中初始化和管理报告package com.yourcompany.framework.base; import com.aventstack.extentreports.ExtentReports; import com.aventstack.extentreports.ExtentTest; import com.aventstack.extentreports.Status; import com.aventstack.extentreports.markuputils.ExtentColor; import com.aventstack.extentreports.markuputils.MarkupHelper; import com.aventstack.extentreports.reporter.ExtentSparkReporter; import com.yourcompany.framework.utils.DriverManager; import com.yourcompany.framework.utils.ScreenshotUtil; import org.testng.ITestResult; import org.testng.annotations.*; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.Date; public class BaseTest { protected static ExtentReports extent; protected ExtentTest test; BeforeSuite public void setUpSuite() { // 设置报告文件路径以时间戳命名避免覆盖 String timeStamp new SimpleDateFormat(yyyyMMdd_HHmmss).format(new Date()); String reportPath System.getProperty(user.dir) /test-output/ExtentReport_ timeStamp .html; ExtentSparkReporter sparkReporter new ExtentSparkReporter(reportPath); sparkReporter.config().setDocumentTitle(Web UI自动化测试报告); sparkReporter.config().setReportName(测试执行报告); extent new ExtentReports(); extent.attachReporter(sparkReporter); extent.setSystemInfo(测试环境, ConfigReader.getProperty(env)); extent.setSystemInfo(浏览器, ConfigReader.getProperty(browser)); } BeforeMethod public void setUpMethod(Method method) { // 每个测试方法开始前在报告中创建一个测试节点 String testName method.getName(); test extent.createTest(testName); test.assignCategory(method.getDeclaringClass().getSimpleName()); // 按类名分组 } AfterMethod public void tearDownMethod(ITestResult result) { // 根据测试结果记录日志和截图 if (result.getStatus() ITestResult.FAILURE) { test.log(Status.FAIL, MarkupHelper.createLabel(测试用例失败: result.getName(), ExtentColor.RED)); test.log(Status.FAIL, 失败原因: result.getThrowable()); // 失败时截图并附加到报告中 String screenshotPath ScreenshotUtil.takeScreenshot(DriverManager.getDriver(), result.getName()); test.addScreenCaptureFromPath(screenshotPath); } else if (result.getStatus() ITestResult.SUCCESS) { test.log(Status.PASS, MarkupHelper.createLabel(测试用例通过: result.getName(), ExtentColor.GREEN)); } else { test.log(Status.SKIP, MarkupHelper.createLabel(测试用例跳过: result.getName(), ExtentColor.YELLOW)); } } AfterSuite public void tearDownSuite() { // 测试套件结束后生成报告 extent.flush(); // 所有测试结束后统一关闭Driver (也可以在AfterMethod中关闭但用ThreadLocal后在监听器中统一清理更安全) } }然后实现一个截图工具ScreenshotUtil.javapackage com.yourcompany.framework.utils; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; public class ScreenshotUtil { public static String takeScreenshot(WebDriver driver, String screenshotName) { String timeStamp new SimpleDateFormat(yyyyMMdd_HHmmss_SSS).format(new Date()); String fileName screenshotName _ timeStamp .png; String directory System.getProperty(user.dir) /test-output/screenshots/; String path directory fileName; try { File sourceFile ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); FileUtils.copyFile(sourceFile, new File(path)); return ./screenshots/ fileName; // 返回相对路径便于报告嵌入 } catch (IOException e) { e.printStackTrace(); return 截图失败: e.getMessage(); } } }报告与日志集成的要点报告路径管理一定要用时间戳或唯一ID命名报告文件否则多次执行会相互覆盖。截图策略不一定每次失败都要截图但对于UI测试截图是定位问题的黄金标准。截图文件名最好包含用例名和时间戳方便追溯。日志分级在框架代码的关键位置如初始化Driver、执行点击操作、发生等待时添加Log4j2的logger.info()或logger.debug()语句。将日志级别设置为DEBUG可以输出非常详细的信息用于排错而在CI环境中可以设置为INFO或WARN减少日志量。与TestNG原生报告结合ExtentReports是独立报告TestNG也会生成自己的index.html报告。两者可以并存ExtentReports更美观TestNG报告则包含了更原始的XML格式结果便于其他工具解析。4. 完整测试执行流程与配置4.1 配置文件详解src/test/resources/config.properties文件是框架的“控制中心”。# 环境配置 envqa base.urlhttps://your-qa-site.com # 浏览器配置 browserchrome # browserfirefox # 超时时间配置 (单位秒) implicit.wait10 explicit.wait15 page.load.timeout30 # 报告配置 report.title自动化测试报告对应的ConfigReader工具类package com.yourcompany.framework.utils; import java.io.FileInputStream; import java.io.IOException; import java.util.Properties; public class ConfigReader { private static Properties properties new Properties(); static { try { FileInputStream fis new FileInputStream(src/test/resources/config.properties); properties.load(fis); fis.close(); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(无法加载配置文件 config.properties); } } public static String getProperty(String key) { return properties.getProperty(key); } }4.2 TestNG XML套件文件我们通过testng.xml文件来组织测试用例的执行比如分组、并行、参数化。!DOCTYPE suite SYSTEM https://testng.org/testng-1.0.dtd suite nameWeb UI自动化测试套件 verbose1 paralleltests thread-count3 !-- paralleltests 表示不同的test标签可以并行执行thread-count控制线程数 -- !-- 也可以 parallelmethods表示方法级别并行但需要确保测试方法是线程安全的 -- test name冒烟测试 preserve-ordertrue groups run include namesmoke/ !-- 只运行标记为‘smoke’组的测试 -- /run /groups packages package namecom.yourcompany.tests.*/ !-- 运行指定包下的所有测试类 -- /packages /test test name登录模块测试 classes class namecom.yourcompany.tests.LoginTest/ !-- 运行指定的测试类 -- /classes /test test name搜索模块测试 classes class namecom.yourcompany.tests.SearchTest/ /classes /test /suite执行方式IDE中右键点击testng.xml选择Run。命令行集成CImvn clean test -DsuiteXmlFiletestng.xml4.3 Maven POM依赖管理pom.xml文件汇集了所有我们需要的库。?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.yourcompany/groupId artifactIdsimple-webui-automation/artifactId version1.0-SNAPSHOT/version properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target selenium.version4.15.0/selenium.version testng.version7.8.0/testng.version extentreports.version5.1.1/extentreports.version log4j2.version2.20.0/log4j2.version poi.version5.2.4/poi.version webdrivermanager.version5.6.3/webdrivermanager.version /properties dependencies !-- Selenium -- dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version${selenium.version}/version /dependency !-- TestNG -- dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version${testng.version}/version scopetest/scope /dependency !-- ExtentReports -- dependency groupIdcom.aventstack/groupId artifactIdextentreports/artifactId version${extentreports.version}/version /dependency !-- Log4j2 -- dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version${log4j2.version}/version /dependency dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-api/artifactId version${log4j2.version}/version /dependency !-- Apache POI for Excel -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version${poi.version}/version /dependency !-- WebDriverManager -- dependency groupIdio.github.bonigarcia/groupId artifactIdwebdrivermanager/artifactId version${webdrivermanager.version}/version /dependency !-- Commons IO for Screenshot -- dependency groupIdcommons-io/groupId artifactIdcommons-io/artifactId version2.13.0/version /dependency /dependencies build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.1.2/version configuration suiteXmlFiles suiteXmlFiletestng.xml/suiteXmlFile !-- 默认执行的测试套件 -- /suiteXmlFiles /configuration /plugin /plugins /build /project5. 常见问题排查与实战技巧5.1 元素定位失败最头疼的问题症状NoSuchElementException,TimeoutException。排查思路按顺序检查等待是否足够这是最常见的原因。首先检查是否使用了WaitUtil中的显式等待。尝试增加等待时间或者更换等待条件如presenceOfElementLocated代替visibilityOf。定位器是否正确页面结构可能已更改。用浏览器的开发者工具F12重新检查元素的ID、Class、XPath或CSS Selector。技巧优先使用id、name等唯一属性。其次是用相对简洁的CSS Selector。XPath尽量少用尤其是包含index或很长路径的XPath非常脆弱。工具Chrome的开发者工具中在Elements面板右键点击元素选择Copy-Copy selector或Copy XPath可以快速获取但需要人工校验其稳定性。元素是否在iframe或shadow DOM中如果在iframe里必须先切换上下文driver.switchTo().frame(“frameNameOrId”)操作完再switchTo().defaultContent()切回来。Shadow DOM更复杂需要用JavascriptExecutor来查找。页面是否完全加载有些页面是动态加载的初始HTML里没有目标元素。确保你的操作触发了数据加载如滚动、点击筛选按钮然后再去定位元素。是否有多个匹配元素findElement只返回第一个匹配项。如果你的定位器匹配了多个元素而你想操作的不是第一个就会出错。检查定位器的唯一性。5.2 测试在CI服务器上失败本地却成功可能原因及解决环境差异CI服务器的浏览器版本、驱动版本可能与本地不同。解决方案使用WebDriverManager它能自动匹配版本。确保CI构建脚本中正确设置了DISPLAY变量对于无头Linux服务器或使用--headless模式。路径问题CI服务器上的项目路径、资源文件路径可能与本地不同。解决方案所有文件路径不要用绝对路径使用System.getProperty(“user.dir”)获取项目根目录然后拼接相对路径。并发问题如果CI上并行执行测试而你的框架或测试用例不是线程安全的。解决方案确保DriverManager使用了ThreadLocal并且测试用例之间没有共享的静态状态如静态变量存储测试数据。资源不足CI服务器内存或CPU不足导致浏览器响应慢。解决方案增加显式等待时间或者在BeforeMethod中给测试方法加Test(timeOut 60000)设置超时。5.3 提升脚本运行速度的技巧使用无头模式Headless在CI或不需要观察UI的调试中使用无头浏览器。ChromeOptions options new ChromeOptions(); options.addArguments(--headlessnew); // Chrome 109 driver new ChromeDriver(options);并行执行在testng.xml中配置paralleltests或parallelmethods并设置合适的thread-count。减少不必要的等待避免全局过长的隐式等待。精确使用显式等待只在需要的地方等待。复用浏览器会话谨慎使用对于非常庞大的测试套件可以考虑在BeforeSuite中初始化一次Driver在所有测试间复用。但这要求测试用例必须能完全独立且一个用例的失败不能影响后续用例需要清理Cookies、LocalStorage等。通常不推荐新手这么做线程安全问题很棘手。5.4 关于“稳定性”的终极心得UI自动化天生比API测试脆弱。除了技术手段流程上的规范同样重要引入元素ID约定与前端开发团队约定为关键的可测试元素如主要按钮、输入框添加唯一的、语义化的id或>