Selenium与Java构建可维护UI自动化测试框架实战指南
1. 项目概述为什么UI自动化测试是研发团队的“刚需”干了这么多年测试我见过太多团队在UI自动化测试上反复折腾。一开始大家热情高涨投入人力搭建框架写了几百个用例结果没过半年维护成本高到吓人页面一改脚本全挂最后只能不了了之留下一堆“祖传”的、没人敢动的自动化代码。这几乎是每个测试团队都会踩的坑。所以今天我们不谈那些高大上的概念就聊聊怎么用Selenium和Java这套最经典、最稳定的组合搞出一套真正能“活下去”、能持续创造价值的UI自动化测试体系。Selenium WebDriver 之所以历经十几年依然是UI自动化的首选核心在于它的“直接”。它通过浏览器原生驱动直接模拟用户操作所见即所得。而Java以其强大的生态、严谨的类型系统和在企业级应用中的深厚根基为自动化测试框架提供了坚实的骨架。两者结合意味着稳定、可控和极强的可扩展性。这个教程的目标就是让你避开那些华而不实的“银弹”掌握一套从零到一、从一到一百都能稳健推进的实战方法。无论你是刚入行的测试新人还是想优化现有自动化体系的资深同学这里的内容都是可以直接“抄作业”的干货。2. 环境搭建与框架选型打造稳固的“地基”很多人一上来就急着写脚本环境随便装装依赖胡乱引入结果跑起来各种诡异问题。磨刀不误砍柴工一个清晰、隔离、可复现的环境是自动化成功的起点。2.1 核心工具链安装与配置首先确保你的机器上安装了JDK 8或11LTS版本长期支持稳定。不建议使用最新版本避免兼容性问题。安装后在命令行输入java -version和javac -version确认。接下来是构建工具。我强烈推荐Maven而非 Gradle 作为入门选择。为什么Maven的pom.xml约定大于配置结构清晰对于测试依赖的管理和项目结构的标准化更友好社区资源也极其丰富。在项目根目录创建pom.xml文件这是所有依赖的“总清单”。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 artifactIdselenium-ui-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 webdrivermanager.version5.6.0/webdrivermanager.version /properties dependencies !-- Selenium Java Client -- dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version${selenium.version}/version /dependency !-- 测试框架TestNG比JUnit更适合测试组织、依赖、参数化 -- dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version${testng.version}/version scopetest/scope /dependency !-- WebDriver管理器自动下载和管理浏览器驱动神器 -- dependency groupIdio.github.bonigarcia/groupId artifactIdwebdrivermanager/artifactId version${webdrivermanager.version}/version scopetest/scope /dependency !-- 日志记录便于调试 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.9/version scopetest/scope /dependency /dependencies /project关键点解析WebDriverManager这个库必须重点介绍。以前我们需要手动下载 ChromeDriver、GeckoDriver还要匹配浏览器版本路径配置繁琐。WebDriverManager 能自动检测系统已安装的浏览器版本并下载匹配的驱动到本地缓存。只需一行代码WebDriverManager.chromedriver().setup();驱动问题从此告别。TestNG vs JUnit对于UI自动化TestNG 更强大。它支持灵活的测试套件定义、测试方法依赖dependsOnMethods、强大的参数化测试DataProvider和丰富的监听器Listener用于生成报告、处理失败截图等这些特性都是大型自动化项目所必需的。2.2 浏览器选择与驱动策略Chrome 是目前自动化测试的绝对主流因为其稳定性、性能和对Web标准支持最好。Firefox 和 Edge 作为交叉浏览器测试的补充。注意永远不要在自动化测试中使用正在用于日常浏览的浏览器配置文件。浏览器缓存、Cookie、插件会导致测试行为不可预测。必须为自动化测试创建并使用独立的、干净的用户数据目录User Data Dir。对于驱动除了使用 WebDriverManager 自动管理在 CI/CD 环境中我们通常会将特定版本的驱动打包到镜像中以确保环境一致性。驱动与浏览器的版本匹配是自动化脚本稳定性的第一道关卡版本不匹配会导致SessionNotCreatedException等错误。3. 核心脚本编写从“能跑通”到“写得稳”环境就绪我们来写第一个脚本。目标不是打印“Hello World”而是完成一个真实的用户场景打开百度搜索关键词验证结果。3.1 第一个可维护的测试用例不要把所有代码都堆在main方法或一个测试方法里。我们从设计一个简单的页面对象Page Object开始。虽然现在流行更复杂的 Page Object Model (POM)但理解其核心思想更重要将页面元素定位和操作封装起来。// BaseTest.java - 测试基类处理WebDriver初始化和销毁 import io.github.bonigarcia.wdm.WebDriverManager; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import java.time.Duration; public class BaseTest { protected WebDriver driver; BeforeMethod public void setUp() { // 1. 自动设置Chrome驱动 WebDriverManager.chromedriver().setup(); // 2. 配置浏览器选项 ChromeOptions options new ChromeOptions(); options.addArguments(--start-maximized); // 最大化窗口 options.addArguments(--disable-infobars); // 禁用“Chrome正受到自动测试软件控制”提示 options.addArguments(--disable-notifications); // 禁用通知 // 强烈建议添加使用无头模式在CI运行可视化调试时注释掉 // options.addArguments(--headlessnew); // 设置独立用户目录避免污染 options.addArguments(user-data-dir/path/to/clean/profile); // 3. 初始化Driver driver new ChromeDriver(options); // 4. 设置隐式等待全局等待策略 driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10)); // 设置页面加载超时 driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30)); } AfterMethod public void tearDown() { if (driver ! null) { driver.quit(); // 使用quit()而非close()quit会关闭所有窗口并终止驱动进程 } } }// BaiduHomePage.java - 百度首页的页面对象 import org.openqa.selenium.By; import org.openqa.selenium.Keys; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.PageFactory; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import java.time.Duration; public class BaiduHomePage { private WebDriver driver; private WebDriverWait wait; // 使用FindBy注解进行元素定位代码更清晰 FindBy(id kw) private WebElement searchInputBox; FindBy(id su) private WebElement searchButton; // 构造函数初始化元素 public BaiduHomePage(WebDriver driver) { this.driver driver; this.wait new WebDriverWait(driver, Duration.ofSeconds(15)); PageFactory.initElements(driver, this); // 初始化FindBy注解的元素 } // 页面加载完成判断 public boolean isPageLoaded() { return wait.until(ExpectedConditions.titleContains(百度一下)); } // 业务方法输入关键词并搜索 public BaiduResultsPage searchFor(String keyword) { searchInputBox.clear(); searchInputBox.sendKeys(keyword); // 两种搜索方式点击按钮或按回车这里演示回车 searchInputBox.sendKeys(Keys.ENTER); // 返回下一个页面的对象实现流程串联 return new BaiduResultsPage(driver); } // 也可以提供点击按钮搜索的方法 public BaiduResultsPage searchByClick(String keyword) { searchInputBox.clear(); searchInputBox.sendKeys(keyword); searchButton.click(); return new BaiduResultsPage(driver); } }// BaiduResultsPage.java - 搜索结果页的页面对象 import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.PageFactory; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import java.time.Duration; import java.util.List; public class BaiduResultsPage { private WebDriver driver; private WebDriverWait wait; // 定位搜索结果容器示例 FindBy(css div.result.c-container) private ListWebElement searchResultItems; // 定位第一个结果的标题 FindBy(css div.result.c-container h3.t a) private WebElement firstResultTitle; public BaiduResultsPage(WebDriver driver) { this.driver driver; this.wait new WebDriverWait(driver, Duration.ofSeconds(15)); PageFactory.initElements(driver, this); } // 验证结果页是否加载 public boolean isResultsLoaded() { return wait.until(ExpectedConditions.visibilityOf(firstResultTitle)).isDisplayed(); } // 获取第一个结果的标题文本 public String getFirstResultTitle() { return firstResultTitle.getText(); } // 验证结果中是否包含特定关键词 public boolean isKeywordInResults(String keyword) { wait.until(ExpectedConditions.numberOfElementsToBeMoreThan(By.cssSelector(div.result.c-container), 0)); for (WebElement item : searchResultItems) { if (item.getText().toLowerCase().contains(keyword.toLowerCase())) { return true; } } return false; } }// SearchTest.java - 最终的测试类 import org.testng.Assert; import org.testng.annotations.Test; public class SearchTest extends BaseTest { Test public void testBaiduSearch() { // 1. 打开百度 driver.get(https://www.baidu.com); // 2. 初始化首页页面对象 BaiduHomePage homePage new BaiduHomePage(driver); Assert.assertTrue(homePage.isPageLoaded(), 百度首页未正确加载); // 3. 执行搜索操作并跳转到结果页 String keyword Selenium 自动化测试; BaiduResultsPage resultsPage homePage.searchFor(keyword); // 4. 验证结果页 Assert.assertTrue(resultsPage.isResultsLoaded(), 搜索结果页未正确加载); String firstTitle resultsPage.getFirstResultTitle(); Assert.assertNotNull(firstTitle, 未获取到第一个结果标题); Assert.assertTrue(resultsPage.isKeywordInResults(Selenium), 搜索结果中未找到关键词Selenium); // 5. 可以添加更多断言例如第一个结果标题是否包含关键词 Assert.assertTrue(firstTitle.contains(Selenium), 第一个结果标题不包含Selenium); } }实操心得PageFactory.initElements这是一个懒加载模式。它不会立即查找所有元素而是在你第一次使用某个FindBy注解的 WebElement 时才去定位它。这提高了初始化速度但要注意如果页面元素动态变化可能需要重新初始化或使用driver.findElement。隐式等待 vs 显式等待implicitlyWait是全局设置告诉WebDriver在查找任何元素时如果没立即找到就轮询等待一段时间这里10秒。而WebDriverWait是显式等待用于等待特定条件如元素可见、可点击、标题包含等。最佳实践是设置一个较短的隐式等待如2-5秒用于处理大多数稳定元素对于关键操作或加载较慢的元素使用显式等待。混用时显式等待优先级更高。driver.quit()vsdriver.close()close()只关闭当前浏览器窗口如果只有一个窗口则关闭浏览器但驱动进程可能还在。quit()会关闭所有关联窗口并终止驱动进程释放资源。测试结束后务必调用quit()。3.2 元素定位八仙过海稳字当头元素定位是UI自动化的基石不稳定的定位是脚本维护的噩梦。优先级由高到低ID唯一且稳定首选。By.id(“kw”)Name常用于表单也比较稳定。By.name(“wd”)CSS Selector功能强大性能好语法灵活。是除ID/Name外的首选。By.cssSelector(“input#kw”)(ID选择器)By.cssSelector(“input.s_ipt”)(Class选择器)By.cssSelector(“input[name‘wd’]”)(属性选择器)By.cssSelector(“div#content_left div.result”)(父子关系)XPath功能最强大可以遍历XML/HTML树但性能稍差且容易因页面结构微小变动而失效。慎用绝对路径以/开头尽量使用相对路径和属性结合。By.xpath(“//input[id‘kw’]”)(相对路径属性)By.xpath(“//button[contains(text(), ‘搜索’)]”)(文本包含)Link Text / Partial Link Text仅用于超链接。By.linkText(“新闻”)Class Name, Tag Name通常不唯一需结合其他条件使用。定位策略黄金法则与开发约定争取为关键测试元素添加唯一的id或>WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(15)); // 等待元素可见并可点击 WebElement button wait.until(ExpectedConditions.elementToBeClickable(By.id(“submitBtn”))); button.click(); // 等待元素存在可能在DOM但不可见 wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(“.loading”))); // 等待元素从DOM中消失如等待加载动画消失 wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id(“spinner”))); // 等待页面标题包含特定文字 wait.until(ExpectedConditions.titleContains(“订单提交成功”)); // 自定义等待条件更灵活 wait.until(d - { String text d.findElement(By.id(“status”)).getText(); return text.equals(“处理完成”); });重要提示对于单页应用SPA如Vue、React页面切换不刷新传统的pageLoadTimeout可能不适用。更需要依赖显式等待等待某个代表新视图加载完成的元素出现如一个特定的组件或数据加载完成的标识。4. 高级技巧与框架封装让自动化脚本“工业级”单个测试用例跑通只是第一步。要让成百上千的用例高效、稳定、易维护地运行需要框架层面的设计。4.1 数据驱动测试硬编码的测试数据是维护的灾难。我们需要将测试数据与脚本逻辑分离。使用TestNG的DataProviderpublic class SearchDataProvider { DataProvider(name “searchKeywords”) public Object[][] provideSearchData() { return new Object[][] { { “Selenium”, true }, { “TestNG”, true }, { “一个不存在的稀奇古怪词XYZ”, false }, // 期望搜索不到结果 { “Java 21”, true } }; } } public class DataDrivenSearchTest extends BaseTest { Test(dataProvider “searchKeywords”, dataProviderClass SearchDataProvider.class) public void testMultiKeywordSearch(String keyword, boolean expectedResultFound) { driver.get(“https://www.baidu.com”); BaiduHomePage homePage new BaiduHomePage(driver); BaiduResultsPage resultsPage homePage.searchFor(keyword); boolean actualResultFound resultsPage.isKeywordInResults(keyword.split(“ “)[0]); // 简单取第一个词 Assert.assertEquals(actualResultFound, expectedResultFound, String.format(“关键词 ‘%s’ 的搜索结果断言失败”, keyword)); } }更复杂的数据可以从外部文件如JSON、YAML、Excel、CSV读取使用像Apache POI(Excel)、Jackson(JSON) 这样的库来解析。4.2 测试报告与失败处理测试不能只靠控制台输出。我们需要直观的报告和失败现场的记录。TestNG原生报告TestNG运行后会生成test-output文件夹内含index.html报告。但比较简陋。ExtentReports这是目前最流行、最强大的开源测试报告库之一可以生成非常美观、信息丰富的HTML报告支持截图、步骤日志、分组、图表等。// 简化的ExtentReports集成示例 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 org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.testng.ITestResult; import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterSuite; import org.testng.annotations.BeforeSuite; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.Date; public class ReportBaseTest extends BaseTest { // 继承之前的BaseTest protected static ExtentReports extent; protected static ThreadLocalExtentTest test new ThreadLocal(); // 支持并行测试 BeforeSuite public void setUpReport() { String timeStamp new SimpleDateFormat(“yyyyMMdd_HHmmss”).format(new Date()); String reportPath “test-output/ExtentReport_” timeStamp “.html”; ExtentSparkReporter sparkReporter new ExtentSparkReporter(reportPath); sparkReporter.config().setDocumentTitle(“UI自动化测试报告”); sparkReporter.config().setReportName(“回归测试套件”); extent new ExtentReports(); extent.attachReporter(sparkReporter); extent.setSystemInfo(“测试环境”, “QA环境”); extent.setSystemInfo(“浏览器”, “Chrome”); } AfterMethod public void afterMethod(ITestResult result) { ExtentTest extentTest test.get(); if (result.getStatus() ITestResult.FAILURE) { extentTest.log(Status.FAIL, “测试用例失败: “ result.getThrowable()); // 捕获失败截图并添加到报告 String screenshotPath captureScreenshot(result.getName()); try { extentTest.addScreenCaptureFromPath(screenshotPath); } catch (IOException e) { extentTest.log(Status.WARNING, “截图添加失败: “ e.getMessage()); } } else if (result.getStatus() ITestResult.SUCCESS) { extentTest.log(Status.PASS, “测试用例通过”); } else { extentTest.log(Status.SKIP, “测试用例跳过”); } extent.flush(); // 确保数据写入 } AfterSuite public void tearDownReport() { if (extent ! null) { extent.flush(); } } private String captureScreenshot(String screenshotName) { String timeStamp new SimpleDateFormat(“yyyyMMdd_HHmmss”).format(new Date()); String fileName screenshotName “_” timeStamp “.png”; Path destPath Path.of(“test-output/screenshots/”, fileName); try { Files.createDirectories(destPath.getParent()); File screenshot ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); Files.copy(screenshot.toPath(), destPath); return destPath.toString(); } catch (IOException e) { e.printStackTrace(); return “”; } } }在具体的测试类中需要在BeforeMethod中创建ExtentTest实例并在测试步骤中用extentTest.log(Status.INFO, “步骤描述”)记录日志。4.3 Page Object Model (POM) 设计模式进阶基础的POM是将页面封装成类。进阶的POM需要考虑组件复用和业务流程封装。组件化将页面上可复用的部分如导航栏、侧边栏、模态框、表格抽象成独立的Component类。页面对象可以包含这些组件对象。public class HeaderComponent { private WebDriver driver; FindBy(css “.user-menu”) private WebElement userMenu; // ... 其他元素和方法 public void logout() { … } } public class DashboardPage { public HeaderComponent header; public DashboardPage(WebDriver driver) { PageFactory.initElements(driver, this); this.header new HeaderComponent(driver); } }业务流程封装将一连串的页面操作封装成一个“业务流”方法对外提供简洁的接口。例如将“登录-进入商品页-加入购物车-结算”封装成OrderFlow.quickBuy(productId)。4.4 并行测试与测试套件管理当用例数量增长串行执行太慢。TestNG 通过testng.xml配置文件轻松支持并行。!DOCTYPE suite SYSTEM “https://testng.org/testng-1.0.dtd suite name“UI自动化套件” parallel“tests” thread-count“3” !-- parallel 可选methods, tests, classes, instances -- !-- thread-count 控制最大并发线程数 -- test name“Chrome测试” parameter name“browser” value“chrome”/ classes class name“com.test.SearchTest”/ class name“com.test.LoginTest”/ /classes /test test name“Firefox测试” parameter name“browser” value“firefox”/ classes class name“com.test.SearchTest”/ /classes /test /suite在BeforeMethod中可以通过Optional注解和ITestContext获取参数动态初始化不同浏览器的Driver。5. 常见问题排查与性能优化即使框架完善脚本运行中依然会遇到各种“坑”。这里记录一些高频问题和解决思路。5.1 典型异常与解决方案速查表异常信息可能原因排查与解决思路NoSuchElementException1. 元素定位器写错或已失效。2. 页面未加载完成元素尚未出现。3. 元素在iframe/frame内。4. 元素被遮挡或隐藏。1. 使用浏览器开发者工具F12重新检查元素属性更新定位器。2.增加合适的显式等待等待元素可见/可交互。3. 使用driver.switchTo().frame()切换到正确的frame。4. 检查是否有弹窗、遮罩层需要先关闭。ElementNotInteractableException1. 元素不可见display:none, visibility:hidden。2. 元素被其他元素覆盖。3. 元素处于不可交互状态如disabled。1. 等待元素变为可见 (ExpectedConditions.visibilityOf)。2. 使用JavaScript直接操作元素((JavascriptExecutor)driver).executeScript(“arguments[0].click();”, element);。3. 检查元素属性确认其disabled属性为false。StaleElementReferenceException你持有的WebElement对象所对应的DOM元素已经“过时”了页面刷新、AJAX更新导致DOM重建。这是POM中最常见的问题之一。解决方案1.重新查找元素在发生操作前用定位器重新获取一次元素引用。2.使用PageFactory的CacheLookup注解但仅适用于几乎不会变的静态元素。3.设计更健壮的方法在页面对象的方法内部每次操作前尝试重新初始化元素或直接使用driver.findElement。TimeoutException显式等待的条件在超时时间内未满足。1. 检查等待条件是否合理页面是否真的按预期变化。2. 增加超时时间但需谨慎过长影响效率。3. 检查网络或应用性能是否是环境问题。4. 考虑使用更宽松的条件如presenceOfElementLocated代替visibilityOf。InvalidSelectorExceptionXPath或CSS Selector语法错误。将定位器字符串复制到浏览器开发者工具的Console中用$x(“your_xpath”)或$$(“your_css”)测试看是否能正确选中元素。5.2 脚本执行慢性能优化点减少不必要的等待评估并缩短隐式等待时间将固定的Thread.sleep替换为条件性的显式等待。优化定位器CSS Selector 通常比复杂的XPath执行更快。避免使用//开头的过于宽泛的XPath。使用无头模式Headless在CI/CD管道或不需要视觉验证时使用--headlessnew参数。浏览器不渲染GUI可节省大量资源和时间。禁用图片/样式加载通过浏览器选项可以禁止加载图片、CSS甚至JavaScript慎用极大提升页面加载速度但可能影响脚本逻辑。ChromeOptions options new ChromeOptions(); HashMapString, Object prefs new HashMap(); prefs.put(“profile.managed_default_content_settings.images”, 2); // 2为禁止 options.setExperimentalOption(“prefs”, prefs);并行化执行如前所述合理利用TestNG的并行特性。重用浏览器会话对于登录态不变的测试序列可以考虑不每次quit()浏览器而是清理Cookie或刷新页面。但这会增加用例间的耦合需权衡。5.3 稳定性提升重试机制与截图对于偶发性的失败如网络波动可以引入重试机制。TestNG 有IRetryAnalyzer接口可以实现。import org.testng.IRetryAnalyzer; import org.testng.ITestResult; public class RetryAnalyzer implements IRetryAnalyzer { private int retryCount 0; private static final int MAX_RETRY_COUNT 2; // 最大重试次数 Override public boolean retry(ITestResult result) { if (retryCount MAX_RETRY_COUNT) { retryCount; System.out.println(“重试测试方法” result.getName() “第 ” retryCount “ 次”); return true; // 返回true表示需要重试 } return false; } }在测试方法上使用Test(retryAnalyzer RetryAnalyzer.class)注解。同时每次失败都必须截图这是定位线上CI失败原因的“黑匣子”。6. 集成CI/CD与容器化自动化测试只有融入开发流程才能发挥最大价值。通常我们将其集成到 Jenkins、GitLab CI、GitHub Actions 等CI/CD工具中。一个简单的Jenkins Pipeline示例pipeline { agent any tools { maven ‘Maven-3.8.6’ jdk ‘JDK11’ } stages { stage(‘Checkout’) { steps { git branch: ‘main’, url: ‘https://your-git-repo.git’ } } stage(‘UI自动化测试’) { steps { script { // 在无头模式下运行测试并生成报告 sh ‘mvn clean test -Dtestng.xmltestng_ci.xml’ } } post { always { // 无论成功失败都归档测试报告和截图 archiveArtifacts artifacts: ‘test-output/**/*’, fingerprint: true // 如果使用了Allure等报告工具可以在这里生成并发布 // allure includeProperties: false, jdk: ‘’, results: [[path: ‘allure-results’]] } } } } }容器化Docker为了获得绝对一致的环境可以将测试代码、依赖和浏览器一起打包进Docker镜像。可以使用官方提供的selenium/standalone-chrome镜像作为基础在其中运行你的测试jar包。这确保了在任何地方本地、Jenkins、K8s运行测试环境都完全一致。踩了这么多年的坑我的最深体会是UI自动化测试的成功技术只占三成流程和规范占七成。没有产品、开发、测试对页面元素稳定性的共识比如约定>