Selenium商城自动化测试框架从零搭建实战与踩坑记录
做了几年电商系统的自动化测试我越来越觉得商城类项目是练手 Selenium 的最佳土壤——页面多、流程长、业务逻辑重登录、搜索、购物车、下单、支付哪怕只是一个小改动回归测试都能跑出几十条用例。早就想写一篇从零到一搭建 Selenium 自动化测试框架的完整实战记录这套方案我在自己负责的模拟项目 X 上完整落地过测试脚本从无到有再稳定运行中间踩过的坑、总结出的套路都整理在这篇里。先交代一下这个实战项目的背景。待测对象是一个某跨平台商城系统包含前台用户端和后台管理端技术选型是常见的单体 Web 应用页面使用 jQuery 做动态渲染大量表单交互、异步数据加载。这样的系统如果全靠手工回归一个版本迭代下来基本要烧掉一整天。而 Selenium 这类浏览器自动化工具正好能模拟真实用户操作点击、输入、下拉选择、滚动、上传文件样样都行。更重要的是它支持多语言绑定Java 是我最常用的生态成熟社区资料多遇到问题几乎都能搜到解法。这篇实战笔记适合三类读者刚接触自动化测试、想在正式项目里落地一套 Selenium 框架的同学已经在写零散脚本、但代码越写越乱、想系统性重构的测试工程师以及需要带新人搭建测试体系的测试组长。我会把从环境准备、目录设计、页面对象建模、用例编写到持续集成的完整链路都讲一遍所有代码都来自实际项目不是教学 Demo 那种花架子。1. 框架选型与整体设计思路1.1 为什么商城系统测试要选 Selenium 而不是其他工具先解决一个问题市面上自动化测试工具不少Cypress、Playwright、Robot Framework 都有人用为什么我这里坚持 Selenium最直接的原因是兼容性和生态。商城系统面向的用户浏览器非常杂尤其是国内环境Chrome、Edge、Firefox、甚至老的 IE 模式都可能有真实用户在用。Selenium 通过 WebDriver 协议跟不同浏览器内核通信一套脚本能跨浏览器跑。这个能力在电商场景里特别重要——用户不会因为你测试到 Chrome 没问题就只用 Chrome你总得验证核心流程在主流浏览器上都正常。其次是语言生态。Selenium 官方支持 Java、Python、C#、Ruby 等语言这意味着测试团队可以复用自身工程能力。我所在的项目组主力语言是 Java同时大家对 TestNG 比较熟所以直接选了 Java Selenium TestNG 这个组合。如果你团队的强项是 Python那用 pytest Selenium 也是完全合理的选择框架思想是相通的。还有一个常被忽略的理由Selenium 的社区积累太厚实了。你遇到的 90% 的元素定位问题、等待问题、弹窗问题在社区里几乎都有现成案例。自动化测试这件事工具的上手难度从来不是瓶颈真正难的是处理各种千奇百怪的页面异常而 Selenium 的经验储备优势在这个时候就体现出来了。当然Selenium 也有明显的短板执行速度比 Playwright 慢因为它是通过网络协议跟浏览器驱动通信对 iframe、多窗口这类场景的处理需要多一点代码量。但对我们这个商城项目来说这些短板都不构成阻碍稳定性优先才是自动化测试框架的第一原则。1.2 技术栈选型的完整清单与取舍依据这里我把最终敲定的技术栈列出来每个组件都说明白为什么选它组件选择选择理由编程语言Java 8团队主力语言静态类型更适合维护大型测试工程自动化框架Selenium 4.x对 Web 元素操作支持最全面兼容多浏览器测试框架TestNG支持用例分组、依赖控制、并发执行、参数化构建工具Maven依赖管理和多模块构建成熟配合 Jenkins 方便断言库TestNG 自带 Assert满足业务校验需求不需要额外引入 Hamcrest数据驱动Apache POI 操作 Excel测试数据和用例脚本分离业务人员也能维护数据日志与报告Log4j2 TestNG Report 自定义截图失败时有日志、截图、报告三层证据链持续集成Jenkins Maven 插件定时触发回归结果自动归档这套组合我用了好几个项目最大的感受是不折腾。每个组件都是各自领域的稳定选择版本兼容性问题少团队成员上手快。特别是 TestNG 的并发执行能力我们后来把用例跑批从 40 分钟压缩到了 12 分钟左右靠的就是它。1.3 目录结构与分层思想为什么必须用 POM很多初学者写自动化是脚本式的一个测试类里写完所有操作先打开页面、再找元素、再点击、再断言。这种写法在用例少的时候没问题但商城项目一迭代问题就全暴露出来了——登录按钮的定位变了你得在十几个脚本里同时修改漏一个就报错维护成本直接爆炸。我落地的这套框架采用经典的 POMPage Object Model模式核心思想是把页面抽象成对象每个页面对应一个类页面上的元素定位和方法封装在类内部测试脚本只关心业务操作不关心元素定位细节。目录结构规划如下selenium-mall-test ├── pom.xml ├── testng.xml ├── src │ ├── main │ │ └── java │ │ ├── config │ │ │ ├── ConfigReader.java # 读取配置文件 │ │ │ └── DriverFactory.java # 浏览器驱动工厂 │ │ ├── pages │ │ │ ├── LoginPage.java │ │ │ ├── HomePage.java │ │ │ ├── SearchResultPage.java │ │ │ ├── CartPage.java │ │ │ └── CheckoutPage.java │ │ ├── components │ │ │ └── TopNavBar.java # 公共导航组件 │ │ ├── utils │ │ │ ├── ExcelDataProvider.java │ │ │ ├── ScreenshotUtil.java │ │ │ └── LogUtil.java │ │ └── testdata │ └── test │ └── java │ ├── cases │ │ ├── LoginTest.java │ │ ├── SearchTest.java │ │ ├── CartTest.java │ │ └── CheckoutTest.java │ └── base │ └── BaseTest.java # 测试基类这个分层最核心的一点用例层永远不出现 By.id 这类定位代码。如果某天页面改版了你只需要动 pages 包里对应的类测试用例一行都不用改。我们在项目里实践过页面结构调整后只改了 3 个页面类30 多条用例零修改继续跑这就是分层带来的直接收益。2. 环境准备与核心配置2.1 浏览器驱动管理的两种方案Selenium 的运作需要浏览器驱动作为中间桥梁。老一代的做法是自己下载驱动文件放到本地然后通过 System.setProperty 指定路径。这种方式的最大痛点驱动版本和浏览器版本强绑定浏览器一升级驱动立刻失效。项目组里五个人四个人的驱动版本不一致跑出的结果都不太一样。我在这个项目里先用了 WebDriverManager 这个库来管驱动Maven 引入依赖后它会自动检测本机浏览器版本下载匹配的驱动省去了手动管理的所有烦恼。dependency groupIdio.github.bonigarcia/groupId artifactIdwebdrivermanager/artifactId version5.6.2/version /dependency使用时只要在 DriverFactory 里做一次初始化即可public class DriverFactory { public static WebDriver createDriver(String browserName) { WebDriver driver; switch (browserName.toLowerCase()) { case chrome: WebDriverManager.chromedriver().setup(); ChromeOptions options new ChromeOptions(); // 无头模式配合 CI 使用 if (ConfigReader.getBoolean(headless)) { options.addArguments(--headlessnew); } options.addArguments(--disable-gpu); options.addArguments(--window-size1920,1080); driver new ChromeDriver(options); break; case edge: WebDriverManager.edgedriver().setup(); driver new EdgeDriver(); break; default: throw new IllegalArgumentException(不支持的浏览器类型 browserName); } driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); return driver; } }注意我在项目中用的是 Selenium 4.x新版本的 headless 模式建议写--headlessnew这样更接近真实浏览器的渲染行为很多旧 headless 模式下才出现的诡异定位问题都能规避掉。2.2 配置文件设计环境切换不再靠改代码商城系统在测试环境、预发布环境、生产环境的域名和账号数据都不同。很多团队的自动化脚本里直接写死 URL换环境就要全量替换非常容易出错。我在框架里做了一个简单的 properties 配置机制不同环境对应不同配置文件。config.properties 设计如下# 环境配置 envtest base.urlhttps://test-mall.example.com # 账号数据 default.usernametestuser01 default.passwordTest123456 # 浏览器配置 browserchrome headlessfalse # 等待超时 implicit.wait.seconds10 explicit.wait.seconds15ConfigReader 是一个简单的静态工具类加载文件后提供 get 方法public class ConfigReader { private static final Properties props new Properties(); static { String env System.getProperty(env, test); try (InputStream in ConfigReader.class.getClassLoader() .getResourceAsStream(config- env .properties)) { props.load(in); } catch (IOException e) { throw new RuntimeException(加载配置文件失败, e); } } public static String get(String key) { return props.getProperty(key); } public static String getBaseUrl() { return props.getProperty(base.url); } }这样设计后切换测试环境只需要在 Maven 命令里加一个-Denvstaging或者通过 Jenkins 参数传入完全不用碰代码。这是自动化测试框架里性价比最高的小改动强烈建议各位一开始就加上。2.3 等待机制隐式等待和显式等待怎么配合等待策略是 Selenium 自动化框架里最容易被忽视、却又直接影响稳定性的部分。商城页面的异步加载场景太多了搜索列表延迟渲染、购物车数量动态刷新、下单后的订单状态异步更新。如果脚本一股脑往下执行大概率会出现 NoSuchElementException 或 ElementNotInteractableException。我的实践经验是隐式等待做兜底显式等待做关键路径。隐式等待在 DriverFactory 里统一设置比如 10 秒它作用于所有 findElement 调用是一个全局安全网。但隐式等待有个问题它只是轮询元素是否出现无法判断元素是否可点击、是否可见、是否被遮挡。所以在核心业务步骤上我封装了一套显式等待工具方法public class WaitUtil { public static void waitForElementVisible(WebDriver driver, By locator, int timeout) { WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(timeout)); wait.until(ExpectedConditions.visibilityOfElementLocated(locator)); } public static void waitForElementClickable(WebDriver driver, By locator, int timeout) { WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(timeout)); wait.until(ExpectedConditions.elementToBeClickable(locator)); } public static void waitForTextPresent(WebDriver driver, By locator, String text, int timeout) { WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(timeout)); wait.until(ExpectedConditions.textToBePresentInElementLocated(locator, text)); } }这里有一个非常容易踩的坑隐式等待和显式等待不要共用同一个 WebDriverWait 实例。WebDriverWait 配合 ExpectedConditions 时如果两者同时作用等待时间可能叠加拖慢执行速度更危险的是在抛出异常时隐式等待会继续轮询导致错误信息延迟数秒才返回排错效率极低。所以我的原则是全局只设一个隐式等待显式等待只在需要的地方局部使用两者的定位器独立管理。3. 页面对象层设计与核心页封装3.1 登录页封装把元素定位收敛到页面类登录是商城系统的基础模块几乎所有业务用例都绕不开。我把登录页单独封装成一个类用 FindBy 注解声明元素定位配合 PageFactory 初始化。这是 Selenium 最经典的用法但要注意 PageFactory 的惰性查找机制——它不是在初始化时就定位元素而是在每次调用元素时才去查找。public class LoginPage { WebDriver driver; FindBy(id username) WebElement usernameInput; FindBy(id password) WebElement passwordInput; FindBy(id loginBtn) WebElement loginButton; FindBy(css .error-msg) WebElement errorMsg; public LoginPage(WebDriver driver) { this.driver driver; PageFactory.initElements(driver, this); } public void login(String username, String password) { usernameInput.clear(); usernameInput.sendKeys(username); passwordInput.clear(); passwordInput.sendKeys(password); loginButton.click(); } public String getErrorMsg() { WaitUtil.waitForElementVisible(driver, By.cssSelector(.error-msg), 5); return errorMsg.getText(); } }有一点要提醒FindBy注解虽然清爽但动态元素比如 id 里带随机参数的不适合用它应该直接用By.xpath或By.cssSelector在方法内部动态构造定位这样反而更灵活。我在商城项目的商品列表里经常碰到 item_123456 这种带动态后缀的元素直接用注解就会失效所以这类场景我都用 By 方法配合显式等待处理。3.2 首页与导航模块公共组件的复用价值商城系统的首页是流量入口但它本身业务逻辑少更多是承载导航跳转。我把顶部导航栏抽成了一个公共组件类 TopNavBar因为首页、搜索结果页、商品详情页、购物车页面都共用这个导航栏把它抽出来单独维护比在每个页面类里重复写一遍搜索框定位要优雅得多。public class TopNavBar { WebDriver driver; FindBy(id searchInput) WebElement searchInput; FindBy(id searchBtn) WebElement searchButton; FindBy(css .cart-icon) WebElement cartIcon; public TopNavBar(WebDriver driver) { this.driver driver; PageFactory.initElements(driver, this); } public SearchResultPage search(String keyword) { searchInput.clear(); searchInput.sendKeys(keyword); searchButton.click(); return new SearchResultPage(driver); } public CartPage goToCart() { WaitUtil.waitForElementClickable(driver, By.cssSelector(.cart-icon), 5); cartIcon.click(); return new CartPage(driver); } }注意这个方法返回的是页面对象这是 POM 模式里一个值得提倡的习惯——页面操作方法的返回类型应该是下一个页面的对象。这带来的直接好处是测试用例可以写出一串流畅的链式调用例如HomePage homePage new HomePage(driver); homePage.getTopNavBar().search(华为手机) .selectProduct(0) .addToCart();可读性非常高几乎就像在描述业务场景。我看到很多团队虽然建了页面类但方法返回值清一色是 void最后用例层还是在手动 new 页面对象、一步一调页面对象的复用价值就被稀释了。3.3 购物车与结算流程多页面联动用例怎么设计商城系统的购物车到结算流程是典型的多页面联动场景。用户从商品详情页加购跳转到购物车页修改数量再进入结算页填写收货地址最后生成订单。这个流程里的元素状态变化频繁而且每一步都依赖上一步的操作结果。在设计购物车相关页面时我特别注意了两点一是购物车列表是一个动态表格不适宜用固定 id 定位而是通过行号或商品名称来定位对应行二是数量调整后小计金额会异步刷新必须等待金额文本更新后再操作下一步。以购物车页为例封装一个修改数量的方法public class CartPage { WebDriver driver; FindBy(css .cart-item) ListWebElement cartItems; public CartPage(WebDriver driver) { this.driver driver; PageFactory.initElements(driver, this); } public void updateQuantity(int itemIndex, int quantity) { // 通过索引定位行再由行内查找数量输入框 WebElement item cartItems.get(itemIndex); WebElement qtyInput item.findElement(By.cssSelector(.qty-input)); qtyInput.clear(); qtyInput.sendKeys(String.valueOf(quantity)); // 触发失焦事件商城系统一般此时会异步重算价格 qtyInput.sendKeys(Keys.TAB); // 等待小计更新 WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.textToBePresentInElementLocated( By.xpath(//div[classcart-item][ (itemIndex 1) ]//span[classsubtotal]), ¥ (quantity * 2999))); } }这里最值得关注的是sendKeys(Keys.TAB)——很多商城系统的价格计算不是绑定 input 事件而是绑定 blur失焦事件。如果你只输入数量就点下一步价格还没来得及重算后面断言就会失败。这个细节我在真实项目里排查了好几个小时才找到根因分享出来希望帮大家省下这段弯路。4. 测试用例编写与数据驱动实践4.1 测试基类设计每个用例的生命周期管家测试基类是框架稳定性的基石。它负责统一创建浏览器驱动、初始化日志、执行用例、采集失败证据、最后安全关闭驱动。我把所有用例公共逻辑都收拢到 BaseTest 里用例子类只需要写自己的业务逻辑。public class BaseTest { protected WebDriver driver; protected Logger log LogManager.getLogger(this.getClass()); BeforeMethod public void setUp() { String browser ConfigReader.get(browser); driver DriverFactory.createDriver(browser); driver.get(ConfigReader.getBaseUrl()); driver.manage().window().maximize(); log.info(浏览器启动完成 browser); } AfterMethod public void tearDown(ITestResult result) { // 用例失败时自动截图 if (result.getStatus() ITestResult.FAILURE) { ScreenshotUtil.capture(driver, result.getName()); log.error(用例失败 result.getName(), result.getThrowable()); } if (driver ! null) { driver.quit(); } } }一个重要的设计点是driver.quit()而不是driver.close()。close 只关闭当前窗口如果用例过程中打开了新窗口close 后浏览器进程还在测试机器的资源会被慢慢耗光。quit 才是彻底关闭整个浏览器进程。这个细节在无人值守的 CI 环境里尤其关键资源泄漏跑几天后机器就卡死了。4.2 业务用例实现登录测试与搜索测试的完整写法有了基类和页面对象用例层就非常清爽了。这里展示登录模块的两类用例——正常流程和异常流程public class LoginTest extends BaseTest { Test(groups {smoke, login}) public void testLoginSuccess() { LoginPage loginPage new LoginPage(driver); loginPage.login(ConfigReader.get(default.username), ConfigReader.get(default.password)); HomePage homePage new HomePage(driver); Assert.assertTrue(homePage.isUserNicknameDisplayed(), 登录成功后应显示用户昵称); } Test(groups {login}) public void testLoginWithWrongPassword() { LoginPage loginPage new LoginPage(driver); loginPage.login(ConfigReader.get(default.username), WrongPass123); String errorMsg loginPage.getErrorMsg(); Assert.assertEquals(errorMsg, 用户名或密码错误, 错误提示文案应正确显示); } }搜索测试会稍微复杂一点因为涉及结果页的校验和断言。搜索结果的校验不只是断言页面没有报错还要断言结果条数、结果相关性、分页逻辑。我用一个辅助方法从结果容器里提取商品名称列表再逐个校验是否包含搜索关键词public class SearchTest extends BaseTest { Test(dataProvider searchKeywords, dataProviderClass ExcelDataProvider.class) public void testSearchByKeyword(String keyword) { HomePage homePage new HomePage(driver); SearchResultPage resultPage homePage.getTopNavBar().search(keyword); ListString productNames resultPage.getAllProductNames(); Assert.assertTrue(productNames.size() 0, 搜索结果不应为空); for (String name : productNames) { Assert.assertTrue(name.contains(keyword), 商品名称应该包含关键词 keyword 实际名称 name); } } }这套用例跑起来后我在真实项目里抓到过好几个回归 bug比如搜索结果为空时页面没有友好提示、商品名称里的品牌字段被截断等都是靠数据驱动把多组关键词批量跑出来的。4.3 Excel 数据驱动把测试数据从代码里剥离数据驱动的核心诉求很简单同一个用例逻辑用不同数据反复跑。商城场景里登录账号有普通用户、VIP 用户、被锁定用户搜索关键词有正常词、单字词、超长词、特殊字符。这些数据如果全写在代码里改数据就要重新编译维护成本太高。我的方案是用 Excel 管理测试数据通过 Apache POI 读取再通过 TestNG 的 DataProvider 喂给用例。ExcelDataProvider 核心实现public class ExcelDataProvider { DataProvider(name searchKeywords) public static Object[][] searchKeywords() throws IOException { String filePath src/test/resources/testdata/search-data.xlsx; ListObject[] data new ArrayList(); try (FileInputStream fis new FileInputStream(filePath); Workbook workbook new XSSFWorkbook(fis)) { Sheet sheet workbook.getSheet(搜索数据); for (int i 1; i sheet.getLastRowNum(); i) { Row row sheet.getRow(i); String keyword row.getCell(0).getStringCellValue(); String expectResult row.getCell(1).getStringCellValue(); data.add(new Object[]{keyword, expectResult}); } } return data.toArray(new Object[0][]); } }配套的 testng.xml 里声明数据提供者后用例跑批时自动展开为多组数据!DOCTYPE suite SYSTEM http://testng.org/testng-1.0.dtd suite name商城自动化测试套件 test name搜索模块 classes class namecases.SearchTest/ /classes /test /suite用 Excel 管理测试数据还有一重意外收获业务人员可以直接打开 Excel 看测试覆盖了哪些数据不需要懂代码。我在项目中跟运营同学协作修改搜索关键词列表时就是直接改 Excel 文件完全不经过开发流程效率提高了一大截。5. 常见问题与排查技巧实录5.1 元素定位不稳定动态属性与多层弹窗的坑做商城自动化绕不开两个定位难题动态属性和嵌套弹窗。我在实际项目中遇到过三种典型情况第一种是元素的 id 或 class 里带时间戳比如div idproduct_1698326400000这种只能通过部分属性匹配的 XPath 处理By locator By.xpath(//div[starts-with(id, product_)]);第二种是页面有多个图层叠加元素在 DOM 里存在但被其他层遮挡点击时 Selenium 会报ElementClickInterceptedException。这种情况靠强制等待是治标不治本正确做法是先把遮挡层关掉或者改用 JavaScript 点击绕过遮挡WebElement element driver.findElement(By.id(submitBtn)); ((JavascriptExecutor) driver).executeScript(arguments[0].click();, element);第三种是弹窗结构不固定有时是 div 模拟弹窗有时是原生 alert。处理这类问题我的经验是先把弹窗的类型判断清楚再决定怎么操作。div 弹窗用普通定位原生 alert 用 driver.switchTo().alert()。5.2 异步加载导致的偶发失败NoSuchElement 的根因分析用例明明本地跑得好好的一放到 Jenkins 上就挂——这是群里被问得最多的问题。我在排查自建 CI 环境时发现偶发的 NoSuchElementException 背后几乎都是异步加载时序问题。举个例子搜索页面点击搜索按钮后结果列表是 AJAX 请求回来后动态渲染的。在某些低配 CI 机器上渲染时间可能达到 3 到 5 秒但用例脚本可能在第 2 秒就去查找结果元素自然就报错了。虽然显式等待设置了 10 秒但如果等待条件写得不对比如等待元素存在而不是元素可见页面元素在 DOM 里挂载了但样式还没渲染出来也会产生不稳定的表现。我的排查套路是这样的先看失败截图和日志时间戳判断是元素完全不存在还是元素存在但不可交互。如果是前者把定位器放进浏览器控制台里跑一下$选择器验证是否正确如果是后者调整等待条件把visibilityOfElementLocated换成elementToBeClickable。不要一上来就加大等待超时时间那样只是掩盖问题脚本会越来越慢。另外强烈建议在 CI 上记录完整的 HTML 页面快照。我在 ScreenshotUtil 里除了截图还把当前页面的 DOM 序列化保存为 HTML 文件排错时直接打开 HTML 快照能清楚看到失败那一刻页面的真实状态比单纯看截图高效得多。5.3 多浏览器并发执行从单测跑到并行跑批的演进商城系统的回归测试用例量上来后串行执行非常耗时。我在这个项目里做了两级加速第一级是 TestNG 的并行执行第二级是 Jenkins 的多节点分发。TestNG 并行很简单在 testng.xml 里设置suite name商城自动化测试 parallelmethods thread-count4但并行执行引入了一个新问题多个浏览器实例同时跑本地资源和端口冲突。Chrome 每个实例大概占 300MB 内存4 个实例同时开普通开发机基本就满了。所以我建议并行度不要超过 4同时给每个线程的浏览器配置独立的 user-data-dir避免缓存冲突options.addArguments(--user-data-dir/tmp/chrome-profile- System.currentTimeMillis());线程安全方面要注意同一个测试类里的 WebDriver 实例不能共享每个测试方法要有独立的 driver。我在 BaseTest 里用的BeforeMethod和AfterMethod天然保证了每个测试方法一套独立上下文这点很重要——如果用了BeforeClass多个测试方法共享一个 driver并行时就会出现浏览器被抢着用的诡异问题。还有一个跑批时的经验加一个失败重试机制。TestNG 的 IRetryAnalyzer 可以让偶发失败的用例自动重跑一次。但重试必须限制次数我一般配置最多重试 1 次并且在日志里明确标记首次失败已重试通过这样既能吸收真实的画面卡顿等偶发因素又不至于掩盖真正的 bug。5.4 常见问题速查表把我在这个项目中踩过的典型问题整理成表格方便大家按图索骥问题现象根因方向解决方案打开页面后立即找元素报错页面尚未完成加载使用 WebDriverWait 显式等待目标元素可见点击按钮被拦截悬浮层/弹窗遮挡元素检查遮挡层并关闭或用 JS 点击下拉菜单选项点不动选项是 hover 后才展开先移动鼠标到父级元素再点击选项输入框明明有值但断言为空输入事件未触发前取值输入后先 sendKeys(Keys.TAB) 触发失焦事件用例在本地通过、CI 挂CI 环境分辨率/渲染差异固定浏览器窗口大小无头模式加显式等待跑批时偶尔闪出多个实例报错并行执行线程冲突给每个线程独立 user-data-dir断言金额不匹配价格异步重算时序问题等待价格文本更新后再断言6. 框架扩展与持续集成落地6.1 报告与通知失败信息第一时间触达一套自动化测试框架如果跑完没人看到结果价值就大打折扣。我的做法是三层报告体系第一层是 TestNG 自带的 HTML 报告它自动生成测试结果汇总包括通过率、失败用例明细、执行时间适合在 Jenkins 上查看。第二层是自定义的截图 日志链路。每个失败的用例自动截图并保存到target/screenshots/目录下同时 Log4j2 输出完整的操作日志包含页面 URL、当前操作步骤、异常堆栈。第三层是消息通知。我给框架加了一个简单的邮件推送模块跑批结束后把统计结果和失败截图压缩包发给测试群。Jenkins 的 Email Extension 插件也能做这件事但要注意只发失败用例的详情全量报告放链接让有需要的人自己点进去看避免邮件轰炸。6.2 Jenkins 定时回归与质量门禁Jenkins 的作用不只是替代手动跑脚本更是把回归测试变成一种质量门禁。我在项目里设了一个策略每天凌晨跑一遍全量回归输出报告开发提交代码并构建通过后自动触发核心冒烟用例smoke 分组10 分钟内给出结果。冒烟没过就阻断发布流程这是自动化测试最有价值的应用方式。Jenkins 的配置核心就两个构建触发器和构建后动作。触发器我用的是定时构建加 Webhook 混合方式Webhook 保证开发提交后立即触发冒烟定时构建保证每天至少跑一次全量回归。构建后动作比较关键的是两个一是归档测试报告HTML 报告和截图目录二是把测试结果发送到邮件组。另外一个特别值得做的优化是把测试报告的可读性提升一下。Selenium 原生报告的样式比较简陋我后来接入了 Allure 报告配合io.qameta.allure:allure-testng依赖跑完自动生成分类清晰的趋势图、失败用例聚类分析。团队日常维护用例时看 Allure 报告比翻 TestNG 报告直观得多这条建议对中大型商城系统尤其适用。写在最后回头看这个商城自动化测试框架的搭建过程我最深的体会是框架的价值不在于代码写得多花哨而在于改一个页面少改十条用例的维护成本优势。项目里的页面对象分层、等待机制、数据驱动这三件套每一项都是被真实业务的复杂度逼出来的不是为设计而设计。如果你正在做类似的商城系统测试建议从小处起步先给登录和搜索两个模块建好页面对象和用例跑通后再逐步扩展购物车、结算、订单这些长链路场景。每一步都要问自己一个问题如果明天页面改了我改几行代码能让用例继续跑答案越少框架越健康。最后分享一个小技巧把测试代码当作生产代码来要求。代码规范、命名清晰、注释到位、日志完整这些在自动化测试项目里经常被忽视但恰恰是团队协作时最宝贵的资产。毕竟自动化测试框架不是一次性脚本它是需要长期迭代、多人维护的工程产物断层的代价会在每一次版本迭代时加倍偿还。