主题
界面
主题
接口测试测的是"服务端对外说的话";从本节起测**"用户在浏览器里看到的东西"**:
接口测试:发 HTTP → 断言 JSON ← 假设"页面正确地把数据显示给用户"
UI 测试:开浏览器 → 点、填、读文本 ← 直接验证"用户真的能用"两者的分工:接口测试覆盖"逻辑",UI 测试覆盖"呈现与交互" ✓ ——本节先解决"怎么让它跑起来" ✓
assumeTrue 跳过与"真失败"的区别 ★★(T2-018);UI 自动化的门槛比接口测试高得多 ——先看四个"还没写测试就卡住"的位置:
| # | 卡在哪 | 现象 |
|---|---|---|
| ① | 浏览器 | 机器上没装 / 版本不对 ✗ |
| ② | 浏览器驱动 | 版本必须与浏览器匹配 ✗✗ |
| ③ | 被测应用 | 它得先跑起来,而且是对的那个版本 ✗ |
| ④ | 等待时机 | 元素还没渲染出来就去找 ✗(T2-009) |
本节处理 ①②③(④ 留给 T2-009)✓
★ 为什么强调"门槛"这件事:
接口测试的失败几乎都是"业务问题";UI 测试的失败有一半是"环境问题" ✓✓
所以 UI 测试的第一课不是写断言,而是"把环境问题与业务问题分开" ✓
① 测试代码(Java + Selenium API)
│ driver.findElement(By.id("submit")).click()
↓
② W3C WebDriver 协议(HTTP + JSON)
│ POST /session/<id>/element/<eid>/click
↓
③ 浏览器驱动(msedgedriver / chromedriver)
│ 一个本地 HTTP 服务,把协议翻译成浏览器能懂的指令
↓
④ 浏览器(Edge / Chrome)
真正执行点击、渲染、执行 JS★ 这个结构解释了三件事:
| 现象 | 原因 |
|---|---|
| 驱动必须与浏览器版本匹配 ★★ | 协议在演进,驱动与浏览器是"配套"的 ✗ |
| 驱动是一个"本地服务进程" ★ | 所以能在任务管理器里看到 msedgedriver.exe ✓ |
| UI 测试慢 | 每次操作都要走一遍"协议 → 驱动 → 浏览器" ✓ |
★ 本轮实测印证了第 2、3 条:
测试运行时:msedgedriver 进程 = 1,msedge 进程 = 8(headless 的多进程架构)这 9 个进程就是"四层结构"的证据 ✓✓ ——UI 测试的成本不是"代码慢",而是"这条链本来就长" ✓
Selenium 4.6+ 内置 Selenium Manager,会自动识别浏览器版本并下载匹配的驱动 ✓ ——这是默认做法 ✓
但它依赖网络 ✗ ——本轮就撞上了 ✓
★ 真实故障记录(原文照抄):
org.openqa.selenium.remote.NoSuchDriverException:
Unable to obtain: msedgedriver, error Command failed with code: 65
error sending request for url (https://msedgedriver.azureedge.net/LATEST_RELEASE_152_WINDOWS)
Driver info: driver.version: EdgeDriver根因:Selenium Manager 要访问微软的驱动 CDN,而当前网络到不了那个域名 ✗✗
★ 关键观察:这个失败表现为 Errors,而不是 Failures ✓✓ —— 因为它是"环境异常",不是"断言不成立" ✓(T2-017 5.2 的区分)
解决方案:指定本地驱动 ✓✓
# 1. 查本机浏览器版本
(Get-Item "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe").VersionInfo.ProductVersion
# → 153.0.4234.48
# 2. 从国内镜像取同版本驱动(npmmirror 有完整归档)
# https://registry.npmmirror.com/-/binary/edgedriver/153.0.4234.48/edgedriver_win64.zip
# 3. 解压到项目内,并在启动测试时指定
mvn test -pl ui-testing -Pui-tests "-DargLine=-Dwebdriver.edge.driver=<绝对路径>\msedgedriver.exe"★ 两种方式的取舍:
| 自动(Selenium Manager) | 手动(本地驱动) | |
|---|---|---|
| 优点 | 零配置 ✓ | 离线可用、版本可控 ✓✓ |
| 缺点 | 依赖网络 ✗✗ | 需要自己管版本 ✗ |
| 适用 | 开发机、能上网的 CI ✓ | 受限网络、内网 CI ✓✓ |
★ 一条实践经验:
把驱动当作"环境依赖"来管理,而不是"让工具自己去搞" ✓✓
因为"自己去搞"失败时,你得到的是一个
NoSuchDriverException,而不是"网络不通" ✗
| headless(无头)★ | 有头 | |
|---|---|---|
| 界面 | 不显示窗口 | 显示 ✓ |
| 速度 | 快 ✓ | 慢(要渲染)✗ |
| 调试 | 看不到现场 ✗ | 能看着它跑 ✓ |
| 适用 | CI、日常回归 ✓✓ | 写/调测试用例时 ✓ |
配置方式:
EdgeOptions options = new EdgeOptions();
options.addArguments("--headless=new", "--disable-gpu", "--no-sandbox", "--window-size=1280,900");
WebDriver driver = new EdgeDriver(options);★ 三个参数的作用:
| 参数 | 作用 |
|---|---|
--headless=new ★ | 新版无头模式(老写法是 --headless,行为不同 ⚠️) |
--disable-gpu | 无头下禁用 GPU 加速(避免部分环境的异常) |
--window-size | 固定视口大小 ——否则默认尺寸会让响应式布局出现不同元素 ★★ |
★ --window-size 常被忽略但很关键:
同一个页面在 1280 宽和 800 宽下,可能显示不同的元素(响应式设计)✓
不固定视口大小 → 定位结果随环境变 → flaky ✗✗
★ 一个真实的提醒:
headless 的"看不见"是双刃剑:它能跑得很快,但你得靠"截图 + 页面源码"来看现场 ✓
所以 T2-011 的"失败留证"在 headless 下是必需的,不是可选的 ✓✓
在写任何业务断言之前,先用一个"欢迎页"验证环境 ✓ ——这样失败时能立刻区分"环境问题"与"业务问题" ✓✓
本项目就是这么做的(labs/web-basics 提供了一个极简自检页 /ui/hello):
@Test
@DisplayName("Edge headless 能打开页面并读到标题")
void edge_headless_should_open_page() {
EdgeOptions options = new EdgeOptions();
options.addArguments("--headless=new", "--disable-gpu", "--no-sandbox");
driver = new EdgeDriver(options); // ① 浏览器 + 驱动能起来吗
driver.get(BASE_URL + "/ui/hello"); // ② 被测应用可达吗
String greeting = driver.findElement(By.id("greeting")).getText(); // ③ 能读到元素吗
assertEquals("UI 环境自检", driver.getTitle());
assertTrue(greeting.contains("你好"));
}三件事,各自失败的含义完全不同:
| # | 验证 | 失败含义 |
|---|---|---|
| ① | 能创建 WebDriver | 浏览器/驱动问题 ✗(不是被测系统的问题) |
| ② | 能导航到页面 | 应用没起 / 地址错 / 网络 ✗ |
| ③ | 能读到元素文本 | 页面结构或渲染问题 ⚠️ |
★ 本项目的实测结果:
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 25.45 s
--- in cn.kb.learn.ui.UiEnvironmentCheckTest三个自检测试全过 ✓ ——它证明的正是"浏览器 + 驱动 + 应用 + 定位"这条链是通的 ✓✓
★ 一个反面的教训:
如果不单独做自检,第一次跑测试时你会同时面对四个未知量 ✗
自检的作用是把它们逐个钉死 ✓✓
Selenium 支持多种定位方式,本节先建立印象,T2-009 会用真实计时对比它们:
| 方式 | 写法 | 适用 |
|---|---|---|
| id ★★ | By.id("submit") | 唯一且稳定 ✓ |
| name ★ | By.name("product") | 表单字段 ✓ |
| cssSelector ★★ | By.cssSelector("p.summary") | 灵活、性能好 ✓ |
| xpath | By.xpath("//td[@class='cell-product']") | 能表达"文本等于"这类条件 ✓ |
| linkText / partialLinkText | By.linkText("登录") | 链接 ✓ |
[data-testid] ★★★ | By.cssSelector("[data-testid='create-submit']") | 测试专用钩子,与前端的样式/结构解耦 ✓✓ |
★ data-testid 值得单独强调:
CSS 类名会被前端为了样式而改动,id 会被为了布局而改动 ✗
data-testid是为"测试"保留的契约 ✓ ——它变更的唯一理由就是"页面真的变了" ✓✓
本项目的页面就用了它:
<button type="submit" id="create-btn" data-testid="create-submit">创建订单</button>这一小节比 5.1 ~ 5.5 都重要 ——因为"用错地方"是 UI 自动化最常见的失败 ✓
UI 自动化的三个固有代价:
| 代价 | 说明 |
|---|---|
| 慢 ★★ | 每个操作走"协议 → 驱动 → 浏览器";本项目 22 个用例跑了 256 秒(平均 11.6 秒/例) |
| 脆 ★★ | 依赖 DOM、视口、异步时序、浏览器版本 ✗ |
| 贵 ★ | 要维护浏览器与驱动版本、要维护选择器 ✗ |
★ 所以判断原则是:
能用接口测试覆盖的,就不要用 UI 测试 ✓✓
UI 测试只留给"只有通过界面才能验证的东西" ✓
| 该用 UI 测 | 不该用 UI 测 |
|---|---|
| 页面能否渲染、元素是否正确显示 ✓ | 金额计算是否正确 ✗(接口测) |
| 表单校验提示是否出现 ✓ | 权限校验逻辑 ✗(接口测) |
| 点击后是否跳转/弹窗 ✓★ | 列表排序算法 ✗(单元测) |
| 响应式布局在不同视口的表现 ✓ | 各种边界值 ✗(接口测,参数化更快) |
★ 一个实用的数量级经验:
单元测试 数百~数千个 秒级
接口测试 数十~数百个 分钟级
UI 测试 几个~几十个 十分钟级 ← 只覆盖"关键用户路径"本项目的数据印证了它 ✓:
| 类型 | 用例数 | 耗时 |
|---|---|---|
| 单元测试(TA) | 116 | 秒级 |
| 接口测试(TB) | 4 | 21.85 s |
| UI 测试 | 22 | 256 s ✓ |
★ 结论:UI 测试的"性价比"最低 ——所以它必须"少而精" ✓✓
EdgeOptions options = new EdgeOptions();
options.addArguments("--headless=new");
WebDriver driver = new EdgeDriver(options);
driver.get("http://localhost:18090/ui/hello");
System.out.println(driver.getTitle());
driver.quit(); // ★ 一定要 quit,否则浏览器进程会残留见 5.4 的代码 ✓
第一次运行:8 个用例全红 → 分不清是"页面坏了"还是"驱动没装" ✗✗被测页面:labs/web-basics/src/main/java/cn/kb/learn/web/ui/UiPageController.java (提供 /ui/hello 自检页与 /ui/orders 订单管理页)
测试代码:labs/ui-testing/src/test/java/cn/kb/learn/ui/UiEnvironmentCheckTest.java
运行(脚本会自动打包、启动应用、跑测试、停应用):
pwsh -File scripts\ui-test.ps1实测输出(真实运行结果):
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 25.45 s
--- in cn.kb.learn.ui.UiEnvironmentCheckTest★ 运行时会看到的现象(可以用任务管理器验证四层结构):
msedgedriver.exe 1 个 ← 驱动(本地 HTTP 服务)
msedge.exe 8 个 ← 浏览器(headless 的多进程架构)
java.exe ← 测试 JVM★ 本轮踩的坑与解法:
| 现象 | 根因 | 解法 |
|---|---|---|
NoSuchDriverException + azureedge.net 不可达 | Selenium Manager 无法访问微软 CDN | 从 npmmirror 取同版本驱动,用 -Dwebdriver.edge.driver 指定 ✓ |
| 测试报 404,但页面代码明明写了 | 跑的是上次留下的旧 jar | 脚本"总是重新打包" ✓(T2-018 说的"环境不一致") |
第二条尤其值得记住:
"改了没生效"是环境类问题的头号伪装 ✓ —— UI 测试尤其容易中招,因为应用是"另一个进程",它的代码版本与测试代码的版本是两件事 ✓✓
msedgedriver 与 msedge 进程 ★ ——这是四层结构的实证 ✓;--headless=new 去掉:看到浏览器窗口自己操作 ✓ ——调试定位问题时非常有用 ★★;--window-size 去掉:观察响应式布局下元素是否变化 ★;quit():观察浏览器进程残留 ⚠️(生产环境的常见事故);driver.get() 的地址改成错的:观察异常类型(NoSuchDriverException?WebDriverException?)★。| # | 错误 | 说明 |
|---|---|---|
| 1 | 驱动版本与浏览器不匹配 ✗✗ | 最经典的失败(5.2)✗ |
| 2 | 依赖自动下载而不做离线兜底 ✗ | 受限网络直接崩(本轮)✗ |
| 3 | 忘了 driver.quit() ✗✗ | 浏览器进程残留、内存泄漏 ⚠️ |
| 4 | 不固定 --window-size ✗ | 响应式布局让定位结果不稳定 ✗(5.3) |
| 5 | 不做环境自检就写业务断言 ✗✗ | 失败时分不清环境与业务(5.4)✗ |
| 6 | 拿旧 jar 测试 ✗✗ | "改了没生效"(第 7 节)✗✗ |
| 7 | 用 UI 测"接口能测的东西" ✗✗ | 又慢又脆(5.6)✗✗ |
目标:把"四层结构"从概念变成你亲手验证过的事实。
步骤:
scripts\ui-test.ps1,同时打开任务管理器,记录 msedge 与 msedgedriver 的进程数 ★;--headless=new 去掉跑一次,看浏览器自己操作 ★★;Errors 还是 Failures ★★;验收方式:能说出"你这台机器上 UI 测试的四层分别是什么"。
扩展任务:给项目加"一键准备环境"脚本:
https://registry.npmmirror.com/-/binary/edgedriver/)✓;Errors 而不是 Failures?EdgeOptions options = new EdgeOptions();
options.addArguments("--headless=new");
WebDriver driver = new EdgeDriver(options);
driver.get("http://localhost:18090/ui/hello");
assertEquals("UI 环境自检", driver.getTitle());
// 没有 driver.quit()这段代码有什么问题? 5. (编程)给 UiEnvironmentCheckTest 加一个测试:验证视口大小确实生效(读取 window.innerWidth 并与设置的 --window-size 对比)。
driver.quit() ✗ ——浏览器进程会残留 ✓ ——用例一多就会堆积大量 msedge 进程,把机器拖慢 ✓★driver.manage().window().getSize(),或用 executeScript("return window.innerWidth") ✓--window-size ★★;data-testid 为什么比 CSS 类名稳定 ★★;《Selenium 元素定位与显式等待》:从"能跑起来"到"能稳定地找到元素"。