
前阵子一直在帮某内部团队维护一套博客后台系统版本迭代快得离谱平均两周一个小版本一周一个bugfix。最痛的不是排查bug而是每次发版前手动回归那几十条核心用例。登录注册、发文、评论、分类、标签、分页挨个点一遍少说一个上午就没了。后来实在顶不住专门花了两个周末给这套博客系统搭了 Selenium 自动化测试把核心主流程全部跑通顺手把测试用例沉淀成了一套可以直接复用的模板。这篇文章就把整个过程拆开讲讲从需求识别、用例设计到代码落地包括踩过的坑一次性说清楚。1. 项目整体设计与测试范围拆解自动化测试最忌讳一上来就写代码。先想清楚三个问题测什么、用什么测、哪些不值得测。1.1 博客系统的核心功能清单我们这个博客系统的功能其实不算复杂典型的个人博客 轻量管理后台梳理下来核心模块有这么几个用户认证注册、登录、退出登录、修改密码文章管理发布新文章、编辑文章、删除文章、文章列表加载文章展示文章详情页、分页浏览、按标签或分类筛选评论系统发表评论、回复评论、评论列表展示后台管理仪表盘概览、文章管理页、用户管理页功能看着不多但真要把每个模块的正向反向用例都覆盖一遍工作量其实不小。这时候就要做取舍。1.2 自动化用例的筛选标准我筛自动化用例的标准很简单三个字高频、稳定、核心。高频每次发版都要手动回归一遍的流程比如登录、发布文章这些必须自动化核心一旦出错直接影响主流程的功能比如用户登录不了那其他功能再多也白搭稳定功能本身已经迭代稳定很少改动的模块优先测那些设计稿还在改来改去的页面自动化做早了纯属浪费按照这个标准我最初圈定的自动化范围是注册登录、文章发布全流程、评论主流程、列表分页浏览。像修改密码这种低频操作暂时先不纳入自动化范围等后续稳定了再补。我用的测试环境是某实验室内部搭建的测试环境数据可以随意造不会污染线上数据。1.3 技术栈选型与原因技术选型上我直接选了最主流的组合Selenium WebDriver Java TestNG Maven浏览器用 Chrome。原因很简单Selenium WebDriver 是行业内做 Web UI 自动化的标配资料多、踩坑的人多、解决方案也全Java 生态里 TestNG 配合 Maven用例管理、依赖管理、测试报告都比较成熟团队其他人也熟悉 JavaChrome 自带的 DevTools 日志和 headless 模式对调试和分析很友好浏览器驱动这块我没用传统的手动下载 chromedriver而是用了 WebDriverManager这个工具会根据本地浏览器版本自动下载对应的驱动省掉了最头疼的版本不匹配问题。这一点强烈建议推荐尤其是团队里多人开发时每个人的 Chrome 版本可能不一样手动管理驱动几乎必踩坑。2. 测试用例设计从需求到可执行逻辑用例设计是整个自动化测试的灵魂。代码写得好不好是效率问题用例设计得好不好是方向和覆盖问题。2.1 用例分层冒烟、核心、扩展我把博客系统的测试用例分成三个层级层级范围运行频率示例冒烟用例最高频主路径每次代码提交后登录成功、登录失败提示核心用例主干业务流每天回归一次发布文章到前台展示闭环扩展用例边界和异常场景每周一次特殊字符、超长输入、数据校验冒烟用例只覆盖最核心的几条路径目的是快速发现系统是否还有基本可用性。核心用例则把一条完整业务链路全部串起来。扩展用例是锦上添花量力而行。2.2 用例设计实例用户登录模块登录是几乎所有系统测试的起点拿它来举例最典型。正向用例正确用户名 正确密码断言跳转到后台首页且显示用户昵称正确用户名 正确密码断言会话状态已建立访问需要认证的页面不被弹回登录页反向用例正确用户名 错误密码断言出现“用户名或密码错误”提示且停留登录页不存在的用户名 任意密码断言给出同样的错误提示避免暴露用户是否存在用户名空白、密码为空断言有对应的必填校验提示登录失败后 URL 不应跳转到后台页防止出现逻辑漏洞我一般建议把登录失败后的停留页断言也加上。有些团队只断言错误提示文本忽略了页面跳转状态如果后端逻辑有问题提示信息对了但页面被偷偷跳走了用例照样应该失败才对。所以一个完整的登录用例要同时断言提示信息、当前URL、页面关键元素。2.3 数据驱动与参数化用例设计博客系统里注册场景非常适合用数据驱动方式。用户名规则、密码强度、邮箱格式这些校验规则散落在大量相同结构的用例里没必要一个个硬编码写死。我做了个测试数据表格里面准备了这些数据分组合法输入组正常用户名、合规密码、正确邮箱期望注册成功边界输入组用户名刚好到最大长度、刚好到最小长度期望注册成功非法输入组用户名含特殊符号、邮箱格式不对、密码不含数字、密码全是空格期望注册失败并出现对应校验提示重复数据组已存在的用户名期望提示已被占用TestNG 里通过DataProvider注解直接把这些数据喂给同一套测试逻辑就行。测试数据放在 XML 文件里管理后续新增场景或调整边界值时不用改代码只改数据文件非常省事儿。2.4 断言设计原则断言有效性写 Selenium 用例断言设计好不好直接决定用例的可靠性。最典型的问题是只断言有没有不断言是不是正确的。举几个我踩过坑的例子断言登录成功后“页面包含某些文本”但这个文本在登录失败页也出现了说明断言没生效断言页面元素存在但因为页面懒加载元素实际是在页面渲染前就存在于 DOM 中结果根本验证不了可见性断言错误提示弹窗出现但没有验证提示内容结果系统弹的是另一个错误用例还绿着我现在的原则是能用文本断言就不用存在性断言能断言具体业务结果就别只看提示信息。比如测试发布文章成功不仅要断言“发布成功”的提示文案还要去文章列表页验证这篇文章确实出现在列表中。验证的闭环拉得越长用例的可信度越高。3. Selenium 自动化测试落地实操设计做完了接下来就是动手写代码。这一节是完整的实操记录从环境准备到页面对象封装每一步都直接可参考。3.1 环境准备需要准备的东西JDK 8 以上版本我用的是 JDK 11稳定且兼容性好Maven 3.6 以上IntelliJ IDEA 或 Eclipse任意顺手就行Chrome 浏览器 WebDriverManager自动匹配驱动版本博客系统测试环境地址我这边是某实验室的内网测试环境http://test.blog.internal/前面几步都比较常规不多说。我重点说下 WebDriverManager在 pom.xml 里引入后代码里只需这样写WebDriverManager.chromedriver().setup(); WebDriver driver new ChromeDriver(options);这样会自动在本地下载和当前 Chrome 完全匹配的 chromedriver 二进制文件再也不会出现“Chrome 升级了驱动不匹配”的问题了。3.2 Maven 工程创建与依赖配置新建一个标准的 Maven 工程结构如下blog-selenium-test/ ├── pom.xml ├── src/ │ ├── main/java/com/blogtest/pages/ # 页面对象类 │ ├── main/java/com/blogtest/utils/ # 工具类 │ └── test/java/com/blogtest/cases/ # 测试用例类我用的是 Selenium 4.x TestNG 7.xpom.xml 关键依赖如下dependencies dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.12.1/version /dependency dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version7.6.1/version scopetest/scope /dependency dependency groupIdio.github.bonigarcia/groupId artifactIdwebdrivermanager/artifactId version5.5.3/version /dependency /dependencies这里注意一个点Selenium 4 开始很多老代码用的WebElement定位直接用了新的相对定位器比如WebDriverWait的写法也变了。依赖版本之间有一定兼容性关系最好跟着官方文档走别自己乱配版本。3.3 浏览器驱动初始化和统一管理我建了一个DriverManager工具类统一负责浏览器的启动、配置、关闭。重点是 ChromeOptions 的配置下面是实际用到的配置项public class DriverManager { private static WebDriver driver; public static WebDriver getDriver() { if (driver null) { WebDriverManager.chromedriver().setup(); ChromeOptions options new ChromeOptions(); options.addArguments(--window-size1920,1080); options.addArguments(--disable-notifications); options.addArguments(--ignore-certificate-errors); options.addArguments(--start-maximized); driver new ChromeDriver(options); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10)); } return driver; } public static void quitDriver() { if (driver ! null) { driver.quit(); driver null; } } }有几个配置项值得解释一下disable-notifications是禁用浏览器的通知弹窗这类弹窗极易干扰元素点击操作我在测试评论系统时就被这个坑过一次ignore-certificate-errors用于测试环境证书不安全时避免页面打不开start-maximized固定窗口大小保证页面布局和元素定位的稳定性。不同屏幕分辨率下布局会变元素位置也跟着变自动化用例会无故挂掉3.4 基于 Page Object Model 的页面封装Web UI 自动化最佳实践就是Page Object ModelPOM核心思想是每个页面封装一个类页面里的元素定位和操作逻辑都写在对应类中测试用例只关注业务流程不关注元素长什么样。以登录页面为例public class LoginPage { private WebDriver driver; FindBy(id username) private WebElement usernameInput; FindBy(id password) private WebElement passwordInput; FindBy(xpath //button[contains(text(), 登 录)]) private WebElement loginBtn; FindBy(css .alert-error) private WebElement errorTip; 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); loginBtn.click(); } public String getErrorTipText() { return errorTip.getText(); } }PageFactory.initElements是 Selenium 自带的支持简化了元素定位的初始化过程。这里有个使用细节FindBy初始化的元素在每次使用前Selenium 会自动去页面里重新查找所以不用自己去封装查找逻辑。但有一点要注意页面发生跳转后WebElement对象的引用可能失效这种情况下需要重新初始化页面对象或者重新获取目标元素。3.5 测试用例的实际编写拿登录成功和登录失败两个用例来展示public class LoginTest extends BaseTest { Test public void testLoginSuccess() { LoginPage loginPage new LoginPage(driver); loginPage.login(test_user_001, Test12345); DashboardPage dashboard new DashboardPage(driver); Assert.assertEquals(dashboard.getCurrentUrl(), http://test.blog.internal/admin/dashboard); Assert.assertTrue(dashboard.getUserName(), containsString(test_user_001)); } Test public void testLoginWrongPassword() { LoginPage loginPage new LoginPage(driver); loginPage.login(test_user_001, wrong_pass); Assert.assertEquals(loginPage.getErrorTipText(), 用户名或密码错误); String currentUrl driver.getCurrentUrl(); Assert.assertTrue(currentUrl.contains(/login)); } }这里用了BaseTest作为所有测试类的父类里面统一放了BeforeMethod让每个用例启动浏览器并打开登录页AfterMethod做失败截图和浏览器退出。有截图这个细节很关键用例失败时把当时的页面截图保存下来排查问题时能省非常多的时间。3.6 发布文章的完整业务链路用例博客系统最核心的链路是登录后台 → 进入文章管理 → 点击新建文章 → 编辑标题和正文 → 保存发布 → 到前台确认文章可见。这类用例不能拆开单独跑因为单独测发布没有验证前台结果等于只测了一半。我把整条链路写在同一个测试方法里截图、断言都随着流程走这也是 POM 实践中的最佳案例。Test public void testPublishArticleFullFlow() { String articleTitle 自动化测试文章_ System.currentTimeMillis(); String articleContent 这是一篇由 Selenium 自动化发布、用于验证完整链路的测试文章。; // 登录 new LoginPage(driver).login(appConfig.getAdminUser(), appConfig.getAdminPwd()); // 进入文章管理页并新建文章 ArticleManagePage articlePage new ArticleManagePage(driver); articlePage.clickNewArticleButton(); // 填写文章信息并发布 ArticleEditPage editPage new ArticleEditPage(driver); editPage.inputTitle(articleTitle); editPage.inputContent(articleContent); editPage.selectCategory(自动化测试); editPage.selectTag(Selenium); editPage.clickPublishButton(); // 断言后台提示发布成功 Assert.assertTrue(editPage.getPublishSuccessMsg().contains(发布成功)); // 到前台列表验证文章可见 HomePage homePage new HomePage(driver); homePage.navigateToFrontPage(); Assert.assertTrue(homePage.getArticleList().contains(articleTitle)); }使用时间戳拼唯一标题并指定一个固定测试分类是为了让每次执行时文章都是可定位、不冲突的。如果每次用固定标题重复执行时可能遇到重名或者被前面轮次的数据干扰用例就会不稳定。4. 常见问题与排查技巧实录自动化测试写出来不难难的是让它长期稳定地运行。我在这套项目里碰到并解决过不少问题下面这些最典型直接做成问题速查表。4.1 元素定位失败的常见原因与解法自动化用例失败的头号原因就是元素定位失败。常见根源有以下几种失败原因现象解决策略动态 ID明明代码定位到了但一旦刷新页面或重新登录元素就找不到了优先改用相对定位策略如 XPath 中的contains()方法匹配稳定的文本或属性页面懒加载元素内容在滚动后才出现在 DOM 中先用Actions滚动到元素位置再执行点击或查找切换 iframe元素在富文本编辑器或弹窗的 iframe 内部直接查不到先driver.switchTo().frame()切到对应帧操作完切回默认内容时间点未到元素已经存在但不可交互用显式等待等待元素可点击而不是隐式等待固定时间博客系统里最容易踩的是富文本编辑器的坑。我用的编辑器是基于 iframe 的内容区在编辑器的 iframe 里直接 findElement 肯定找不到。解决方法是先定位到 iframe 的 WebElement然后切进去再操作内容区。4.2 等待策略隐式等待与显式等待关于等待我见过太多同学刚开始图省事全局设一个implicitlyWait(10)就完事了。这种做法的问题在于它只等待元素出现不等待元素可见或可点击。很多 AJAX 内容加载完成后元素在 DOM 里出现了但还处于隐藏状态直接点击就会报ElementClickInterceptedException。所以我的习惯是全局隐式等待给个短的兜底值比如 5 秒关键交互操作前用显式等待。显式等待的写法非常简洁WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(15)); wait.until(ExpectedConditions.elementToBeClickable(publishButton)).click();像发布文章后等待成功提示出现、等待列表加载完这种动作全都要用显式等待覆盖。我在实际项目里有一条铁律一切与时间相关的判断都要用显式等待不写具体的Thread.sleep()。Thread.sleep()是测试不稳定的源头之一网速快慢不同、服务器负载不同固定的 sleep 时间永远不可能完美适配。4.3 StaleElementReferenceException 的处理这个异常在 Selenium 里非常典型页面发生了刷新或者跳转之前拿到的 WebElement 引用失效了。我之前写过一个文章列表的翻页用例第一次跑得好好的第二次就跑挂了报的就是这个错。原因是翻页后列表 DOM 被重新渲染旧元素引用全部失效。解决办法有两种路径翻页后重新定位关键元素不用之前保存的引用在用到元素引用前再做一次显式等待让 Selenium 重新封装我在项目里采用的是第一种每次操作完成后都主动重新定位。虽然多写几行代码但稳定性极强。另外补充一个和它容易混淆的异常ElementNotInteractableException。这个异常表示元素能定位到但不可交互。常见于隐藏的菜单、被遮罩层覆盖的按钮。处理方法是用 JavaScript 强制点击或者先关闭弹层JavascriptExecutor executor (JavascriptExecutor) driver; executor.executeScript(arguments[0].click();, targetElement);这招在极端情况比较管用但不要把它当成日常手段。如果能用正常方式点击优先用正常方式。强 JS 点击绕过了一些页面事件绑定逻辑可能导致点击后行为不一致。4.4 chrome 运行模式与稳定性优化测试用例跑在本地跑着跑着页面多起来系统内存和 CPU 占用就会变高测试也容易偶发失败。我在后期把用例统一改成了 headless 模式就是无头浏览器模式不显示界面后台运行。这样资源占用更小跑得也更快。options.addArguments(--headless); options.addArguments(--no-sandbox); options.addArguments(--disable-gpu);改成 headless 后有一类问题要注意页面截图全是空白或者页面渲染时元素出现时机和正常模式不一致。我当时排查了半天发现是--disable-gpu参数导致的页面渲染异常去掉这个参数就好了。这属于经验问题了如果跑 headless 时遇到图片加载不全、截图空白优先检查这两个参数的组合。4.5 测试数据隔离与清理策略自动化用例跑多了必然产生大量测试数据。我测博客系统时每跑一轮就会多出几十篇测试文章、几百条测试评论。如果不做清理测试环境的数据会越堆越多影响后续用例的执行结果。我用的清理策略是每次全量回归前先通过后台的管理接口或直接清库把上次测试产生的脏数据全部清理掉。再有一个细节测试账号的昵称和头像也尽量做到独一无二每次执行都生成新的随机后缀避免不同轮次的用例相互干扰。注意清理测试环境的数据之前一定先确认这是不是专用的测试环境。内部团队容易犯的错是“环境复用”一个环境多个项目共用数据清理会误删别人的测试数据。4.6 用例失败后的定位与排查最后分享一个非常实用的排查思路。Selenium 用例失败时本身的信息往往不够日志里有个StaleElementReferenceException或者TimeoutException但看不出到底是哪一步逻辑出了问题。我在 BaseTest 里做了一层增强任何用例失败时自动截图并保存 HTML 源码AfterMethod public void tearDown(ITestResult result) { if (result.getStatus() ITestResult.FAILURE) { File screenshot ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); String fileName target/screenshots/ result.getName() _ timestamp .png; File destFile new File(fileName); try { FileUtils.copyFile(screenshot, destFile); System.out.println(失败截图已保存: destFile.getAbsolutePath()); } catch (IOException e) { e.printStackTrace(); } } DriverManager.quitDriver(); }HTML 源码是经常被忽略的一个信息源。截图只能看到画面HTML 能看到页面完整的 DOM 状态。比如某元素明明在截图中出现了但代码里就是定位不到看 HTML 才知道元素被弹窗挡住了或者被隐藏了。我从这个里面排查出的问题远远多于日志本身。5. 一些关于投入产出的体会整套博客系统的 Selenium 自动化测试项目从需求梳理到代码落地到跑稳定我前后用了两个周末加几个晚上。初期搭建成本主要花在用例设计、POM 封装和等待策略的调整上这些地方一旦做好了后面增量加用例就是非常机械的工作复制一个模板改改元素定位和断言十分钟能加一条用例。后期最大的收益不是省了那几次手工回归时间而是让团队成员有勇气在临近发版前改代码。以前临近发版没人敢动共享模块一改就担心影响别的功能。现在有自动化测试托底全量回归从一上午压缩到十几分钟越改越放心。不过也要泼一盆冷水自动化测试不是万能的。博客系统里很多场景不适合做 Selenium 自动化比如界面细节的视觉比对、需要兼容多种异常网络环境的场景、以及那种很久才出现一次的偶发线上问题。这类问题用单元测试、接口测试或者人工抽测来解决成本更低效果更好。如果你想基于这套思路复现类似项目我给的建议是先抓核心主链路跑通不要一上来就追求全覆盖。把登录、发布、评论、列表浏览这四条主链路跑通跑稳这个自动化框架的价值就已经体现出来了。再往后测试数据和用例模板的沉淀才是最值钱的部分。