前阵子一直在帮某内部团队维护一套博客后台系统,版本迭代快得离谱,平均两周一个小版本,一周一个bugfix。最痛的不是排查bug,而是每次发版前手动回归那几十条核心用例。登录注册、发文、评论、分类、标签、分页,挨个点一遍,少说一个上午就没了。后来实在顶不住,专门花了两个周末给这套博客系统搭了 Selenium 自动化测试,把核心主流程全部跑通,顺手把测试用例沉淀成了一套可以直接复用的模板。这篇文章就把整个过程拆开讲讲,从需求识别、用例设计到代码落地,包括踩过的坑,一次性说清楚。
1. 项目整体设计与测试范围拆解
自动化测试最忌讳一上来就写代码。先想清楚三个问题:测什么、用什么测、哪些不值得测。
1.1 博客系统的核心功能清单
我们这个博客系统的功能其实不算复杂,典型的个人博客 + 轻量管理后台,梳理下来核心模块有这么几个:
- 用户认证:注册、登录、退出登录、修改密码
- 文章管理:发布新文章、编辑文章、删除文章、文章列表加载
- 文章展示:文章详情页、分页浏览、按标签或分类筛选
- 评论系统:发表评论、回复评论、评论列表展示
- 后台管理:仪表盘概览、文章管理页、用户管理页
功能看着不多,但真要把每个模块的正向反向用例都覆盖一遍,工作量其实不小。这时候就要做取舍。
1.2 自动化用例的筛选标准
我筛自动化用例的标准很简单,三个字:高频、稳定、核心。
- 高频:每次发版都要手动回归一遍的流程,比如登录、发布文章,这些必须自动化
- 核心:一旦出错直接影响主流程的功能,比如用户登录不了,那其他功能再多也白搭
- 稳定:功能本身已经迭代稳定,很少改动的模块优先测;那些设计稿还在改来改去的页面,自动化做早了纯属浪费
按照这个标准,我最初圈定的自动化范围是:注册登录、文章发布全流程、评论主流程、列表分页浏览。像修改密码这种低频操作,暂时先不纳入自动化范围,等后续稳定了再补。我用的测试环境是某实验室内部搭建的测试环境,数据可以随意造,不会污染线上数据。
1.3 技术栈选型与原因
技术选型上我直接选了最主流的组合:Selenium WebDriver + Java + TestNG + Maven,浏览器用 Chrome。
原因很简单:
- Selenium WebDriver 是行业内做 Web UI 自动化的标配,资料多、踩坑的人多、解决方案也全
- Java 生态里 TestNG 配合 Maven,用例管理、依赖管理、测试报告都比较成熟,团队其他人也熟悉 Java
- Chrome 自带的 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.x,pom.xml 关键依赖如下:
<dependencies> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.12.1</version> </dependency> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>7.6.1</version> <scope>test</scope> </dependency> <dependency> <groupId>io.github.bonigarcia</groupId> <artifactId>webdrivermanager</artifactId> <version>5.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-size=1920,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 Model(POM),核心思想是:每个页面封装一个类,页面里的元素定位和操作逻辑都写在对应类中,测试用例只关注业务流程,不关注元素长什么样。
以登录页面为例:
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", "Test@12345"); 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 自动化,比如界面细节的视觉比对、需要兼容多种异常网络环境的场景、以及那种很久才出现一次的偶发线上问题。这类问题用单元测试、接口测试或者人工抽测来解决,成本更低,效果更好。
如果你想基于这套思路复现类似项目,我给的建议是:先抓核心主链路跑通,不要一上来就追求全覆盖。把登录、发布、评论、列表浏览这四条主链路跑通跑稳,这个自动化框架的价值就已经体现出来了。再往后,测试数据和用例模板的沉淀,才是最值钱的部分。