资讯详情

资讯详情

Java Selenium实战:网页表格排序功能自动化验证全攻略

做 Web 自动化测试的朋友多半都踩过表格排序的坑。表格排序功能看起来真不复杂不就是点一下表头数据按照升序或者降序重新排一遍吗但真落到自动化验证上你会发现在十个项目里起码有六个能从这个“简单功能”里找出点幺蛾子。我自己用 Java Selenium 写过不少表格排序的自动化验证从一开始只会“点一下、抓一列、跟排序前对比一下”到后来被各种偶发失败折腾到怀疑人生才慢慢总结出一套相对稳的验证方案。这篇文章就围绕“使用 Java Selenium 验证网页表格数据排序功能”这个主题把这套思路完整展开。内容适合刚学会 Selenium 元素定位的测试新人也适合已经能写脚本、但总觉得排序断言写得不够踏实的测试开发。你要的其实不只是“跑通脚本”而是让排序验证变得可复用、能解释、不玄学。1. 排序功能为什么容易翻车痛点与测试策略1.1 从一次线上问题说起之前有个项目后台管理系统的列表页所有排序逻辑都在前端用 JavaScript 做。上线后用户反馈“金额排序是错的我按金额降序结果一笔 1,000,000 元的单子跟 2,000 元的单子混在一起2,000 还排前面。”我们一开始怀疑接口返回有问题后来查了代码发现前端用的是字符串比较不是数值比较。字符串比大小是按字符一位一位比1000000的开头是12000的开头是2所以 100 万会排在 2,000 前面完全反了。这种问题手点起来反而不是特别容易发现因为数字少的时候习惯靠“肉眼感觉”判断要么看几行觉得没毛病就过了要么恰好扫到某个异常数据才发现。自动化测试的价值就在这里用同一个规则去校验整列数据不靠人眼不靠抽查。1.2 排序验证到底在验证什么很多人写排序自动化时思路是“点击前抓一列数据点击后再抓一列数据然后对比两边不一样就说明排序生效了”。这个思路有个根本问题它只验证了“数据变了”没有验证“数据变对了”。我后来会把排序验证拆成至少五个层面点击表头后排序事件确实被触发表格重新渲染或者数据重新请求了。目标列的所有行数据都能被正确提取出来没有缺行、错位、遗漏。提取出来的值经过类型归一化后整体顺序满足升序或降序的规则。其他列的数据仍然跟主排序列保持同一行没有出现整列漂移。切换点击次数、切换排序列、翻页等场景下排序结果依然稳定。只抓到“顺序变了”但没检查“顺序对不对”等于白跑。1.3 在 UI 层做排序验证为什么值得有段时间团队想省成本直接调接口验证排序结果。接口层能验证后端排序逻辑但验证不了前端点击事件、前端二次排序、表格渲染绑定这些环节。排序是一个强交互行为UI 层才是最贴近用户真实感受的位置。虽然 UI 自动化跑起来慢、容易受环境干扰但排序类用例的覆盖价值远大于维护成本。我的建议是用 UI 层用例覆盖“点击表头→展示排序结果”这条主链路接口层再补充大数据量、边界数据的排序逻辑测试。两层都过了才能算真稳。2. 搭建可复用的排序验证骨架环境与核心思路2.1 Maven 依赖与 Selenium 版本选择Java 项目做 Selenium最常见也是我最推荐的方式是用 Maven 管理依赖。Selenium 4 跟 3 的 API 有一些差异尤其是等待时长和窗口管理建议新项目直接用 4.x 版本。dependency groupIdorg.seleniumhq.selenium/groupId artifactIdselenium-java/artifactId version4.21.0/version /dependency dependency groupIdio.github.bonigarcia/groupId artifactIdwebdrivermanager/artifactId version5.9.2/version /dependency dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version7.10.2/version /dependency dependency groupIdorg.assertj/groupId artifactIdassertj-core/artifactId version3.27.0/version /dependencyWebDriverManager 解决了一个特别烦人的问题浏览器驱动版本跟本地浏览器版本对不上。以前手动下载 chromedriver每次 Chrome 一升级脚本就挂。用 WebDriverManager 之后它会自动匹配本机浏览器版本省心很多。测试框架我用 TestNG原因很简单DataProvider做数据驱动特别方便后面写多列多方向的排序用例时非常好用。2.2 浏览器驱动的选择与启动方式本地调试排序用例我默认用 Chrome因为 Selenium 对 Chrome 的支持最成熟。但是线上 CI 环境经常没有图形界面所以驱动管理这块要写灵活一点WebDriverManager.chromedriver().setup(); ChromeOptions options new ChromeOptions(); options.addArguments(--headlessnew); options.addArguments(--window-size1920,1080); driver new ChromeDriver(options);这里加window-size是因为 headless 模式下默认窗口小表格可能会被折叠或者某些表头吸顶后点击不到。我踩过这个坑最初 headless 模式跑排序点击一直报“element not clickable”加了大窗口和滚动才解决。2.3 抽象一个 TableSorterVerifier 核心类不要把所有排序代码堆在测试方法里。一个页面上可能有多个表格不同表格的列结构也不同最好把“定位表格、触发排序、提取列数据”封装成一个独立类。我通常维护一个TableSorter类构造函数传入WebDriver和表格的By定位器对外暴露sortBy、getColumnValue、waitForReload这些方法。public class TableSorter { private final WebDriver driver; private final By tableLocator; public TableSorter(WebDriver driver, By tableLocator) { this.driver driver; this.tableLocator tableLocator; } public void sortBy(int columnIndex) { WebElement table driver.findElement(tableLocator); WebElement header table.findElement( By.xpath(.//thead//tr[1]//th[ (columnIndex 1) ]) ); ((JavascriptExecutor) driver).executeScript( arguments[0].scrollIntoView({block: center});, header); header.click(); } public ListString getColumnValues(int columnIndex) { waitForTableReady(); WebElement table driver.findElement(tableLocator); ListWebElement rows table.findElements(By.xpath(.//tbody/tr)); return rows.stream() .map(row - row.findElement(By.xpath(./td[ (columnIndex 1) ])).getText().trim()) .collect(Collectors.toList()); } }注意Sorting之后再去调getColumnValues一定要重新driver.findElement(tableLocator)不能缓存之前找到的 table 元素。因为排序基本都会重绘表格缓存元素很容易触发StaleElementReferenceException后面专门讲这个坑。3. 手写一个排序验证器从定位表格到触发排序3.1 表格 DOM 结构与定位策略写任何表格操作前先打开浏览器 DevTools 看 DOM 结构。常见的标准表格是table thead tr th和table tbody tr td。定位表头可以直接用 XPath.//thead//tr[1]//th[2]这里tr[1]表示第一行的表头因为有些表头有两行一级表头是合并单元格二级表头才是真正可点击的列名。如果遇到这种你要先定位到实际触发排序的那个th不能死板地用第一行。定位单元格也是一样.//tbody/tr[i]/td[j]如果列里有td嵌套了span、div、input、a标签直接用getText()通常能拿到可见文本但有时候会拿到子元素里隐藏的文案。最稳的方法是getText().trim()同时配合normalize-space()去清理多余空白。3.2 点击表头触发排序的动作细节看起来就是header.click()但实际上有几个细节会影响稳定性。第一表头可能被顶部吸顶导航遮挡需要先scrollIntoView。第二有些表头内部不是span“金额”/span这种简单结构而是整个th上绑定了点击事件点th本身就能触发有些则需要在th内部的某个图标元素上点击。这就要看开发怎么实现。我会在sortBy方法里尽量让点击动作落在th中央区域并且先滚动到可见位置避免被遮挡。如果点了之后页面没有任何变化不要急着断言先用 Debug 模式看一眼是不是点击事件绑定在子元素上。这种问题靠看代码比靠猜更快。3.3 等待排序完成不能只靠 Thread.sleep很多人写排序用例喜欢点击完直接Thread.sleep(2000)再抓数据。我强烈不建议sleep 一长测试就慢一短就偶发失败。Selenium 里写等待的正确姿势是WebDriverWait轮询但难的是“等什么条件”。排序完成后最常见的变化是第一行数据变了所以可以记录点击前第一个单元格的文本然后等待它变成新值public void waitForReload(String oldFirstCell) { WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(d - { ListWebElement rows d.findElement(tableLocator) .findElements(By.xpath(.//tbody/tr)); if (rows.isEmpty()) return false; String currentFirstCell rows.get(0) .findElement(By.xpath(./td[1])).getText().trim(); return !oldFirstCell.equals(currentFirstCell); }); }这个方案不是万能。如果排序前的第一个值和排序后的第一个值恰好相同条件永远不满足超时了脚本才失败。所以我一般会结合页面上是否出现 loading 遮罩、列表数据是否重新请求这些状态来判断。还有一种更稳健的思路点击排序按钮后找到表格外层的 loading 元素等待它从“显示”变为“隐藏”。很多组件库会有一层半透明遮罩或者某个区域出现 spinner。条件是wait.until(ExpectedConditions.invisibilityOfElementLocated(By.cssSelector(.table-loading)));如果你的项目没有 loading 标识那就在排序前后抓整表的数据签名比如把所有行第一列的值拼接成字符串等待这个签名发生变化。再不行就用“重试 固定间隔”的轮询但频率不要太高500 毫秒一次比较合适。3.4 提取列数据并做类型归一化抓出来的数据是字符串排序断言前必须先做类型归一化。比如金额列页面上是1,000.00如果直接当字符串比较1,000.00和100.00是按字符逐个比的结果完全不可靠。我通常先定义一个ColumnType枚举STRING、NUMBER、DATE、AMOUNT。然后根据列类型写不同的解析方法。金额解析要注意几点去掉千分位逗号1,000.00→1000.00去掉货币符号不只是$还有、€可能有括号表示负数(1,000.00)要解析成-1000.00百分比12.5%可以提取出12.5再除以 100也可以保留看你业务上要验证什么日期解析更麻烦页面上常见2024/01/10、2024-1-10、2024年1月10日。我建议先把各种分隔符统一再交给LocalDate解析。对日期列做排序验证最好让测试数据用同一种格式否则“解析不了”本身就是测试要暴露的问题。4. 排序断言怎么写才能不踩坑规则、边界与反例4.1 升序、降序与自然排序的区别排序断言本质上就是拿一个 Comparator 去比较提取出来的列表。我给每条用例传一个方向参数descending为 false 表示升序true 表示降序。用 AssertJ 的isSortedAccordingTo是最清晰的import static org.assertj.core.api.Assertions.assertThat; public void assertNumericColumnSorted(ListString rawValues, boolean descending) { ListBigDecimal values rawValues.stream() .map(s - new BigDecimal(s.replace(,, ).replace($, ))) .collect(Collectors.toList()); ComparatorBigDecimal comparator descending ? Comparator.reverseOrder() : Comparator.naturalOrder(); assertThat(values).isSortedAccordingTo(comparator); }注意Comparator.reverseOrder()是对自然排序取反。如果里面有null或者解析失败的值BigDecimal构造器会直接抛异常测试会立刻告诉你哪行数据有问题这其实是好事。4.2 字符串列排序的大小写与空白问题字符串列排序很容易踩的坑是大小写。程序里字符串排序默认是按 Unicode 码位排的所以大写字母A会排在a前面降序的时候结果可能跟预期不一致。业务上如果要求忽略大小写你的比较器就要用String.CASE_INSENSITIVE_ORDER。还有中英文混排。英文按 ASCII 码排中文按 Unicode 排大多数业务对中文排序的需求是“按拼音”而前端表格不一定能保证这一点。这种列我们一般单独定义规则不要让通用断言去猜。字符串还要去掉前后空白否则 Apple会排在Banana的后面肉眼以为没排对其实是空白字符干扰。4.3 空值、重复值、超长文本的边界处理表格里经常出现空字符串或者null显示成-。排序断言时如果业务规则是“空值永远排最后”那么比较器里就要显式处理ComparatorString comparator (a, b) - { if (a.isEmpty()) return 1; if (b.isEmpty()) return -1; return a.compareTo(b); };重复值也是一样。如果某列有相同数据的行它们的相对顺序是不确定的不要断言它们必须保持不变。很多组件库的排序是“不稳定排序”同值行顺序会变这是正常的。测试用例里如果用了相同的数据只需要保证整个列表排序规则满足不要去看相同值之间的先后。超长文本列通常截取一部分显示getText()拿到的可能不是完整值。如果排序是根据完整值排的UI 显示的是截断后的文本那在 UI 层直接断言就不可靠。这种情况我会选择抓取元素上的>public ListString getColumnValues(int columnIndex) { WebDriverWait wait new WebDriverWait(driver, Duration.ofSeconds(10)); return wait.until(d - { ListWebElement rows d.findElement(tableLocator) .findElements(By.xpath(.//tbody/tr)); ListString values new ArrayList(); for (WebElement row : rows) { values.add(row.findElement(By.xpath(./td[ (columnIndex 1) ])) .getText().trim()); } return values; }); }wait.until内部即使抛了 StaleElement 异常Selenium 的重试机制也会继续轮询直到条件满足或者超时。这比在测试方法里写一堆 try-catch 干净得多。5.2 表头点击不生效可见区域、滚动与遮挡有些表格表头是固定在页面顶部的点击时元素的中心可能被其他悬浮层盖住Selenium 会报ElementClickInterceptedException。我一般先用 JavaScript 滚动这不是过度设计而是排序用例在 CI 上最容易挂的场景之一。滚动到表格区域后再点击成功率会高很多。还有一个隐蔽问题表头里如果有排序图标图标是 CSS 伪元素::before、::afterSelenium 是点击不到伪元素的。但有些前端组件把点击事件绑定在伪元素上导致你点击th文本区域无效必须点图标附近。这种情况没有银弹只能让开发给表头加一个稳定的 class 或者>DataProvider public Object[][] sortScenarios() { return new Object[][] { {1, ColumnType.NUMBER, false}, {1, ColumnType.NUMBER, true}, {2, ColumnType.DATE, false}, {3, ColumnType.STRING, false}, {3, ColumnType.STRING, true}, {4, ColumnType.AMOUNT, false}, }; } Test(dataProvider sortScenarios) public void verifyTableSort(int columnIndex, ColumnType type, boolean descending) { ListString rawValues tableSorter.getColumnValues(columnIndex); tableSorter.sortBy(columnIndex); tableSorter.waitForReload(rawValues.get(0)); ListString sortedValues tableSorter.getColumnValues(columnIndex); assertColumnSorted(sortedValues, type, descending); }这样维护起来很舒服。哪天新增一列只需要在DataProvider里加一行不用再复制粘贴一段测试方法。6.2 用 Allure 报告记录点击前后的表格快照排序用例最怕“脚本报了失败但说不清楚哪里不对劲”。Allure 是个很好的伙伴。我在断言前会调用Allure.addAttachment(排序前表格, beforeData.toString())在断言失败后打一个页面截图。这样 CI 上挂了打开报告就能看到排序前后到底长什么样。截图逻辑可以写在AfterMethod里判断ITestResult.getStatus() ! SUCCESS时就截图AfterMethod public void takeScreenshotOnFailure(ITestResult result) { if (result.getStatus() ! ITestResult.SUCCESS) { File screenshot ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); try { Allure.addAttachment(失败截图, Files.newInputStream(screenshot.toPath())); } catch (IOException e) { // 忽略截图失败 } } }有了截图遇到偶发问题也更容易跟开发对线。6.3 对接 CI 流水线从本地跑通到每日巡检排序会受数据量影响本地测试库几千条数据排序可能秒开但生产环境几十万条数据接口响应时间可能超过 10 秒。CI 上跑的时候超时时间要比本地放宽 1.5 到 2 倍。我把排序用例单独打成一个 testng 的 group比如sort然后 CI 流水线里每天凌晨跑一次全量每次代码合并前只跑冒烟级的两三条排序用例。这样既不会让每次提交都很慢又能保证线上数据的变化不会悄悄引入排序 bug。WebDriverManager 在 Linux 的 CI 环境里要注意它会自动下载对应浏览器的 driver但前提是 CI 机器上已经安装了 Chrome 或者 Firefox。如果 CI 是 Docker 环境最好把浏览器和 driver 都固化在镜像里不要每次构建都去下载否则网络抖动会直接导致整个排序用例全挂。6.4 扩展思路从“验证排序”到“通用表格校验”这个类封装好之后不只是排序能用。分页后数据是否刷新、筛选后表格是否变化、列拖拽后顺序是否正确都可以复用TableSorter的“定位表格、提取行数据”能力。我后来甚至把TableSorter扩展成了DataTableValidator支持对整表做数值求和、去重、空值检查等操作。排序验证只是表格自动化的一部分但它的思路很典型把 UI 交互动作拆细把等待条件做稳把断言规则跟业务类型对齐。这套逻辑一旦沉淀下来换一个项目、换一种前端框架你只需要改动定位器和解析规则主体代码基本不用重写。最后分享一个提升脚本稳定性的小技巧如果这个表格是你们自己开发团队做的试着推动开发在表头th上加上>
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →