先说实话网上搜“Selenium自动化测试框架搭建”教材一大堆但大部分上来就丢代码环境怎么配、目录怎么分、用例怎么写才不算白写反而没人讲清楚。我之前在某公司带过一阵子自动化测试小组把一套基于Java Selenium的Web自动化框架从零到一搭了起来还靠着这套东西扛过了几轮大版本的回归测试。这篇不像教科书那样啰嗦我会把搭框架的思路、代码结构、关键封装、踩过的坑一整套讲明白。不管你是刚接触Selenium的测试新手还是被脚本维护搞到头疼的初级开发这篇都能让你至少少走一个月的弯路。1. 框架搭建前的整体设计与技术选型1.1 为什么是Java而不是Python聊到Selenium第一反应多半是Python一把梭。确实Python上手快写脚本跟写作文似的但在企业级测试框架这个场景下Java的底子更厚。我们当时选Java核心理由有三条。第一被测系统本身就是Java技术栈测试团队和开发团队用同一种语言很多底层工具类可以直接参考甚至复用开发那边的代码习惯沟通成本低。第二Java生态里的TestNG、Maven、Jenkins配合非常成熟尤其是TestNG的参数化、依赖管理、并行执行能力在跑大规模回归用例时优势极其明显。第三Java毕竟是强类型语言写出来的测试代码在重构和维护上有天然的约束力团队里五六个测试工程师同时维护一套框架也不容易各写各的变成一锅粥。当然如果你团队里全是Python选手或者被测系统是个轻量级活动页Python完全没问题。工具没有绝对好坏只有合不合适。我们当时面对的是一个模块特别多、业务规则复杂的管理后台需要长期稳定迭代最终选了Java这条路事实证明这个决定还算正确。1.2 核心组件选型测试引擎、构建工具、自动化驱动框架离不开几样东西测试引擎管用例执行和结果上报构建工具管依赖和打包Selenium本身负责驱动浏览器干活。我把这套组合列出来都是经过实际验证的组件选型选型理由自动化库Selenium Java 4.x对浏览器支持最全面4.x版本引入了相对定位器等新特性定位效率更高断言库TestNG自带的Assert Hamcrest断言信息更丰富失败时能直接看到期望值和实际值的差异排查问题快测试引擎TestNG支持分组、依赖、参数化、并行执行策略灵活比JUnit更适合测试场景构建工具Maven依赖管理清晰配合Maven Surefire跑测试非常顺滑Jenkins集成方案最成熟浏览器驱动管理WebDriverManager自动下载匹配的Driver省去手动配环境的痛苦还支持浏览器版本过期自动升级这里特别说一下WebDriverManager它是我们框架里“省心系数”最高的一环。以前没有它的时候Chrome一升级ChromeDriver版本不匹配整套回归用例直接全部报错只能人等手动换驱动文件。引入WebDriverManager之后它会自动识别当前浏览器版本、自动下载驱动虽然首次执行会耗点时间但换来的稳定收益非常可观。1.3 一个合理的自动化测试框架应该长什么样我见过太多人搭框架刚写了两三个页面就跑过来问为什么维护困难。原因很简单脚本写死的逻辑太多改个登录框的ID所有用例跟着遭殃。成熟的框架至少应该具备三个特征。第一定义清晰的分层结构用例层、业务层Page Object层、基础设施层Driver管理、日志、报告层与层之间单向依赖。第二可靠性设计等待策略、失败重试、截图定位这些机制要有统一的基础实现而不是每写一条用例就重复一遍。第三数据与代码分离测试数据放在外部文件或配置文件里代码不写死任何业务数据换环境、换数据时用例本身不用动。这套思路落到代码上目录结构通常长这样src ├── main │ └── java │ ├── config // 配置类 │ ├── driver // 浏览器驱动管理 │ ├── page // 页面对象层 │ ├── util // 工具类、日志、截图 │ └── base // 测试基类 └── test └── java ├── tests // 测试用例 └── resources // 测试数据、配置文件记住这个分层逻辑后面的代码全部按这个结构展开。2. 环境准备与Maven工程初始化少踩依赖坑2.1 JDK、Maven与IDE的版本搭配建议这套框架对JDK的版本要求不算苛刻Java 8以上都能跑。但这里有个细节值得注意Selenium 4.x本身是基于Java 8编译的而TestNG最新版和Maven插件在某些组合下对JDK版本有要求。我当时用的搭配是JDK 11 Maven 3.8.x IDEA社区版整体跑得很稳。如果你是新项目直接上JDK 17也行但要注意尽量别用过于新的小版本某次我升到JDK 17.0.6之后Maven编译一切正常但TestNG的报告生成插件莫名其妙不工作后来才发现是某个依赖没跟上回退到17.0.2就好了这种属于典型的依赖版本兼容问题。Maven的settings.xml建议配置阿里云或国内镜像仓库不然后续下载依赖的速度会让人怀疑人生。Selenium核心依赖加上TestNG、WebDriverManager、ExtentReports这些全部拉下来小几十MB是正常的没有镜像仓库首次构建等五分钟都是常事。2.2 通过Maven构建基础项目骨架在IDEA里新建Maven项目然后往pom.xml里塞下面这些依赖。依赖不是越多越好我见过有人把几十个库全塞进来最后冲突到怀疑人生。我们实际用到的核心依赖一只手数得过来properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding selenium.version4.9.0/selenium.version testng.version7.7.1/testng.version /properties dependencies !-- Selenium核心库 -- dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version${selenium.version}/version /dependency !-- 测试引擎 -- dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version${testng.version}/version scopetest/scope /dependency !-- 浏览器驱动自动管理 -- dependency groupIdio.github.bonigarcia/groupId artifactIdwebdrivermanager/artifactId version5.4.1/version scopetest/scope /dependency /dependencies版本号建议用属性统一管理这样后期升级一个入口就能搞定不用满文件搜替换。还有一个易踩的坑selenium-java本身会把selenium-api、selenium-remote-driver等一堆子模块带进来不要重复声明这些子模块否则可能出现同一类从两个jar包里加载的诡异问题。依赖配置好之后执行mvn clean compile验证编译通过。这一步之后如果IDEA语法检查一堆红多半是没点刷新Maven项目手动刷新一次就好别急着怀疑代码写错。顺便提醒IDEA里的File - Settings - Build - Compiler - Build project automatically建议勾上改完Java代码自动编译跑测试的启动速度会快很多。2.3 第一个冒烟测试验证Selenium能驱动浏览器框架还没搭完的时候先写一个最简单的测试验证整套环境是通的避免后续框架代码写了一大堆才发现是环境问题。我一般在项目根目录建一个SimpleSmokeTest.java代码如下import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.testng.annotations.Test; public class SimpleSmokeTest { Test public void testOpenPage() { WebDriver driver new ChromeDriver(); driver.get(https://example.com); System.out.println(页面标题: driver.getTitle()); driver.quit(); } }直接运行这个TestNG用例如果浏览器能弹出来并打印出页面标题说明环境链路已经打通。但这里有个很重要的细节new ChromeDriver()之所以能找到浏览器驱动是因为我在整个项目引用了WebDriverManager但仅仅引用还不够需要先调用它的配置方法。实际上Selenium 4.6之后内置了Selenium Manager会自动处理驱动但为了框架的统一性和可控性我在正式代码里还是会显式用WebDriverManager管理。第一次跑冒烟测试时大概率会遇到版本不匹配的报错这个在第五节专门讲。冒烟测试通过后恭喜环境问题已经干掉一大半。后面要做的所有事情都是在这个基础上一步步搭起骨架。3. 核心封装DriverFactory、日志与统一等待策略3.1 DriverFactory设计把Driver创建交给一个工厂很多人写自动化测试每个测试类里都来一句new ChromeDriver()这其实是最危险的操作。创建浏览器实例不是免费的它要启动进程、加载配置、建立会话。如果每条用例都新建浏览器几十条用例跑下来内存和CPU开销非常夸张还容易因为浏览器窗口开太多导致机器卡死。更合理的做法是整一个DriverFactory负责统一创建和管理浏览器实例。我当时的实现思路是本地调试用ChromeDriver跑无头模式headless用同一个DriverFactory切换配置从环境变量或配置文件读取当前要用的浏览器类型。代码长这样public class DriverFactory { private static ThreadLocalWebDriver driverThreadLocal new ThreadLocal(); public static WebDriver getDriver() { if (driverThreadLocal.get() null) { driverThreadLocal.set(createDriver()); } return driverThreadLocal.get(); } public static WebDriver createDriver() { WebDriver driver; String browserType ConfigReader.get(browser.type, chrome); switch (browserType.toLowerCase()) { case chrome: default: WebDriverManager.chromedriver().setup(); ChromeOptions options new ChromeOptions(); options.addArguments(--start-maximized); if (Boolean.parseBoolean(ConfigReader.get(browser.headless, false))) { options.addArguments(--headlessnew); } driver new ChromeDriver(options); break; case firefox: WebDriverManager.firefoxdriver().setup(); driver new FirefoxDriver(); break; } driver.manage().window().maximize(); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(15)); return driver; } public static void quitDriver() { if (driverThreadLocal.get() ! null) { driverThreadLocal.get().quit(); driverThreadLocal.remove(); } } }这里有两个点值得说道。第一ThreadLocalWebDriver是并行执行用例时保证线程安全的关键每个线程拿到的Driver实例互相独立互不干扰。第二--headlessnew是Chrome新版本的无头模式参数我实测比老的--headless稳定得多特别是在跑截图和下载测试的时候旧模式偶尔会触发一些奇怪的渲染问题。窗口最大化也要注意UI自动化里元素能否被点击受窗口大小影响很大最大化窗口能减少很多因为元素被遮挡导致的点击失败。3.2 统一等待策略不要到处写Thread.sleep犯过最大的错是什么就是刚学自动化的时候定位不到元素就扔一个Thread.sleep(3000)进去。三个用例能忍三十条用例跑下来光等待时间就吃掉近十分钟而且这种写法极其脆弱机器性能好一点就会跳过元素出现的瞬间机子差一点就还没加载完。成熟框架里要有一套统一等待策略。Selenium里等待分显式等待和隐式等待两者不能混用。隐式等待是全局的但有个缺陷它只管元素是否存在不管元素是否可点击、可见。显式等待则灵活得多能指定等待条件比如元素可点击或元素文本变成期望值。我的做法是封装一个WaitUtils类统一基于WebDriverWait实现public class WaitUtils { public static void waitForElementVisible(WebDriver driver, By locator, int timeoutInSeconds) { WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(timeoutInSeconds)); wait.until(ExpectedConditions.visibilityOfElementLocated(locator)); } public static void waitForElementClickable(WebDriver driver, By locator, int timeoutInSeconds) { WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(timeoutInSeconds)); wait.until(ExpectedConditions.elementToBeClickable(locator)); } public static WebElement getElement(WebDriver driver, By locator, int timeoutInSeconds) { waitForElementVisible(driver, locator, timeoutInSeconds); return driver.findElement(locator); } }所有用例都通过这个工具类获取、操作元素等待策略统一可控。实际项目中我把默认超时时间设为15秒对于正常的Web系统已经足够充裕。如果某些接口响应特别慢再针对单个元素单独加长等待时间而不是全局拉长。特别提醒Thread.sleep不是完全不能用比如等待某个动画播放完、等待异步任务刷新但必须用在明确知道需要固定时间等待的场景平时定位元素一律用显式等待。把这条规则写进团队的代码规范里能减少一半以上的不稳定用例。3.3 日志与截图失败时自动留证据自动化测试最气人的场景是用例挂在凌晨的Jenkins构建里早上来一看只看到一行ElementClickInterceptedException完全不知道当时页面上到底发生了什么。截图和日志就是给这种时刻准备的后悔药。我在框架里做了一个TestWatcher接口监听TestNG的测试失败事件自动截图并记录日志。核心逻辑不复杂就是拿到当前Driver实例调用getScreenshotAs把图片存到以测试类名和当前时间戳命名的文件里public class ScreenshotUtil { public static String captureScreenshot(WebDriver driver, String screenshotName) { String dirPath target/screenshots; File dir new File(dirPath); if (!dir.exists()) dir.mkdirs(); File screenshot ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); String filePath dirPath / screenshotName _ System.currentTimeMillis() .png; try { FileUtils.copyFile(screenshot, new File(filePath)); } catch (IOException e) { log.error(截图失败, e); } return filePath; } }截图位置放在target目录下刚好被Maven构建产物统一管理Jenkins归档报告时顺手就把截图收集走了。日志我用的是Logback分两个层级控制台输出简洁的运行进度文件日志记录完整的DOM操作和异常堆栈。遇到问题时先看截图再看日志里定位元素的关键信息排查效率能翻好几倍。4. 页面对象封装与测试用例编写实战4.1 Page Object模式把页面元素和操作绑在一起一行测试用例里夹杂五个定位表达式这种代码在项目上存活超过一个月基本就没法维护了。页面元素一变所有用到它的用例全得跟着改谁改谁崩溃。Page Object模式就是为了解决这个问题一个页面一个类页面上的元素定位和操作逻辑全部收敛到这个类里用例层只关心业务操作。拿登录页来举例public class LoginPage { private WebDriver driver; private By usernameInput By.id(username); private By passwordInput By.id(password); private By loginButton By.id(login-btn); public LoginPage(WebDriver driver) { this.driver driver; } public void inputUsername(String username) { WaitUtils.getElement(driver, usernameInput, 15).sendKeys(username); } public void inputPassword(String password) { WaitUtils.getElement(driver, passwordInput, 15).sendKeys(password); } public HomePage clickLoginButton() { WaitUtils.waitForElementClickable(driver, loginButton, 15); driver.findElement(loginButton).click(); return new HomePage(driver); } }注意clickLoginButton方法返回了HomePage对象这叫页面跳转的语义化表达。用例里这样写LoginPage loginPage new LoginPage(driver); loginPage.inputUsername(admin); loginPage.inputPassword(123456); HomePage homePage loginPage.clickLoginButton();读起来就是一段业务操作流完全没有底层定位的噪音。被测系统的页面特别多的时候这种模式的好处会成倍放大。维护时只改对应的Page类用例层几乎不碰。4.2 用例层与TestNG生命周期绑定接下来是测试基类。所有用例类都继承它在BeforeMethod里初始化Driver和Page对象AfterMethod里处理结果失败时截图最后统一清理Driverpublic class BaseTest { protected WebDriver driver; protected LoginPage loginPage; BeforeMethod public void setUp() { driver DriverFactory.getDriver(); driver.get(ConfigReader.get(app.url)); loginPage new LoginPage(driver); } AfterMethod public void tearDown(ITestResult result) { if (result.getStatus() ITestResult.FAILURE) { ScreenshotUtil.captureScreenshot(driver, result.getName()); } DriverFactory.quitDriver(); } }TestNG的ITestResult参数能拿到测试方法名、状态和异常这是处理失败截图的关键接口。一个典型的登录模块测试类长这样public class LoginTest extends BaseTest { Test(dataProvider loginData) public void testLogin(String username, String password, boolean expectedResult) { loginPage.inputUsername(username); loginPage.inputPassword(password); HomePage homePage loginPage.clickLoginButton(); Assert.assertEquals(homePage.isUserLoggedIn(), expectedResult); } DataProvider(name loginData) public Object[][] loginData() { return new Object[][] { {admin, admin123, true}, {admin, wrongpass, false}, {, admin123, false} }; } }一个小的设计心得是测试方法名尽量用test前缀加上具体的业务行为描述比如testLoginWithWrongPassword这样失败日志里一眼就能看出是哪类功能出了问题不需要翻代码。4.3 数据驱动TestCase从写死的数据中解脱用例跑得越多数据注入的痛感越明显。最早我把测试数据直接写在用例里后来产品提了个需求要加一组新的账号数据回归登录流程我硬着头皮去改Java代码。直到引入数据驱动才算真正解脱。TestNG的DataProvider可以从方法返回二维数组也能配合Excel、YAML、数据库将外部数据文件读进测试用例。我项目里用的方案是Excel维护测试数据然后用一个ExcelDataProvider统一读取于是数据变更不再动代码。public class ExcelDataProvider { public static Object[][] readExcelData(String filePath, String sheetName) { // 用Apache POI读取Excel返回Object[][] // 第一行是表头从第二行开始是数据 } }配合TestNG的dataProvider使用Test(dataProvider loginDataFromExcel) public void testLoginWithExternalData(String username, String password, String expected) { // ... }这里有个关键点用外部数据驱动之后用例规模可以轻松扩张到几百条所以用例要有设计感——不是数据越多越好而是要覆盖典型场景和边界用户比如正确账号、错误密码、空输入、超长输入每一类都至少一条。这样的数据矩阵才算真正的回归弹药库。5. 常见问题排查与踩坑实录5.1 变量一ChromeDriver版本不匹配这个坑每一位Selenium玩家都至少踩过一次。症状非常经典启动浏览器时直接崩掉控制台打出一行类似The version of ChromeDriver only supports Chrome version xxx的报错。原因不复杂浏览器大版本一更新旧的ChromeDriver就失效了。解决方案是引入WebDriverManager并且在每次测试开始前调用WebDriverManager.chromedriver().setup()。它会自动检查当前Chrome的版本动态下载对应的驱动二进制。我在框架里把这段逻辑放进了DriverFactory所以任何人拿到代码只要本机装了Chrome跑起来就是开箱即用不要求手动安装任何驱动文件。5.2 变量二元素明明存在点击却说ElementNotInteractable这是UI自动化里最膈应人的报错之一。页面上按钮看得见自动化一点就报ElementClickInterceptedException或ElementNotInteractableException。原因多到能写一本书但我遇到最多的就两种一是页面上有浮层或遮罩挡住了按钮二是按钮虽然渲染出来了但位置正在发生位移比如下拉菜单展开的动画还没结束。排查步骤也固定了先用getScreenshotAs截图看现场再在当前页面的控制台手动试一下元素确认是不是被遮挡。最终解法是在点击前加上waitForElementClickable等待元素真正可交互并且对于有动画的场景把显式等待的ExpectedConditions.elementToBeClickable换成visibilityOfElementLocated加一个Thread.sleep(300)让动画跑完虽然有点作弊但实测效果稳定。5.3 变量三并发执行时Driver串台框架一开始没加ThreadLocal并行跑用例的时候经常出现A用例的浏览器里跳出了B用例的登录信息或者两个用例共用一个窗口元素没找到。这就是典型的Driver实例串台本质是多个线程共享了同一个WebDriver实例。解决方式在上面DriverFactory那节已经提到了用ThreadLocalWebDriver。每个线程通过getDriver()拿到的都是自己的绑定实例线程结束后调remove()清理。这个改造虽然只有几行代码但它是并行执行能够稳定落地的根基建议在一开始就把ThreadLocal写进去别等到并行铺开了再回头改。5.4 变量四测试报告长腿跑路只剩HTML躺在那报告是自动化测试的“门面”领导看日报、周报全靠它。最早我用的是TestNG自带的HTML报告样式朴素不说跟ExtentReports一比能力差距明显。后来换成了ExtentReports它支持图表统计、历史趋势、用例步骤标记还能把失败截图直接嵌到报告里。配置也不难用ExtentTest记录每个用例的执行结果在BaseTest的AfterMethod里拿到ITestResult把成功、失败、跳过分别标记最后flush生成完整报告。报告文件同样放在target/reports下在Jenkins的Post-build Actions里添加HTML Report插件归档报告文件路径团队就能在CI界面直接看历史趋势了。这一步做完框架的反馈闭环才算完整。6. 框架落地最后一公里与CI/CD的集成和稳定实践6.1 环境配置抽离换环境不是改代码项目一旦跨环境跑本地、测试、预发把环境地址写死在代码里就是给自己埋雷。我封装了一个配置读取工具从config.properties中读全局配置支持环境变量覆盖这样CI机器人里传参就能切环境代码本身不需要任何改动。public class ConfigReader { private static Properties props new Properties(); static { try (InputStream in ConfigReader.class.getClassLoader() .getResourceAsStream(config.properties)) { props.load(in); } catch (IOException e) { e.printStackTrace(); } } public static String get(String key) { String value System.getProperty(key); if (value null) { value props.getProperty(key); } return value; } public static String get(String key, String defaultValue) { String value get(key); return value null ? defaultValue : value; } }这套方案在API自动化里也通用但Web UI自动化还有一个额外的好处环境配置统一管理之后测试数据集也能跟着环境走。比如同一个登录模块测试环境用的是假数据预发环境用的是脱敏后的真实数据数据文件放在不同的目录下通过配置切换一套代码到处跑。6.2 让用例跑得更稳的几条实操纪律框架搭好、用例写完之后真正考验人的是长时间运行的稳定性维护。我把这段时间沉淀的几条实操纪律分享出来每一条都是从线上翻车现场总结出来的。第一条用例之间不要互相依赖。A用例登录后的状态不要作为B用例的前置条件每条用例尽量做到自包含。一旦用例间产生依赖并行和排序都会变得极其痛苦。第二条元素定位优先用稳定属性。id、name这类属性排最前XPath尽量用相对路径不用带index的绝对路径。绝对路径脆得像方便面前端DOM结构调整一下整条XPath就直接失效。第三条断言信息一定要写清楚。Assert.assertEquals(登录成功提示文案, actual, expected)比Assert.assertTrue(condition)的排查价值高不止一个量级。失败信息里多一句话晚上排查问题就能少熬十五分钟。第四条固定频率跑一次全量回归。自动化用例维护跟种地一样常跑常新。一周不跑环境变了、数据坏了至少能在一轮全量回归里发现得早别等版本要发布时才来一波猝不及防。6.3 Maven与Jenkins回归任务一键触发框架跑通后我在Jenkins里建了一个构建任务拉取测试仓库代码执行mvn clean test最后归档HTML报告和截图产物。配置里把TestNG的suite文件路径指定给Surefire插件plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.0.0/version configuration suiteXmlFiles suiteXmlFiletestng.xml/suiteXmlFile /suiteXmlFiles /configuration /plugintestng.xml里维护的是用例分组和运行方式。按业务模块分组回归时只跑当前迭代影响的模块大幅压缩全量跑的时间需要全量回归时再把所有分组点开。这个配置文件也放在Git里业务模块增加了改一下这个XML就能实现新增用例自动纳入回归范围。Jenkins任务跑完之后我还会加一个邮件通知失败超过N条或构建不稳定时自动给相关测试开发发一封邮件附上报告链接和失败列表。这样不用每天手动点进Jenkins页面问题发生时第一时间就能收到联动消息。整个框架的链路到这里才算真正完整。最后说说这套框架实际用下来的感受。数据驱动、等待统一、截图留证、报告自动归集这一套做完整之后最大的受益不是用例跑得快而是人解放出来了——以前熬夜盯回归、手工翻日志的活儿基本没了测试工程师可以把精力放到设计用例场景和业务逻辑分析上去这才是我觉得搭建自动化测试框架最有价值的地方。你团队里的自动化如果还处在脚本满天飞的阶段照着这篇的思路重新梳理一遍稳定性的提升会非常明显。
阅读完成 · 觉得有帮助?