如何为 Comprehensive Rust 的 tests 目录编写避免状态泄漏与竞态条件的 WebdriverIO 测试
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
Comprehensive Rust 的课程网站(theme/下的book.js、speaker-notes.js等 JS 代码)依赖真实浏览器行为来验证功能,因此项目的tests/目录用 WebdriverIO 搭配 Mocha 和 WebdriverIO Expect API 编写端到端测试。新写或维护测试时最容易遇到两个问题:前一个测试留下的状态(如 JavaScript 写入的行内样式、sessionStorage)泄漏到下一个测试,以及页面元素由 JavaScript 在初始加载后动态生成、测试提前访问导致的偶发失败。这篇文章给出仓库中实际使用的两类写法,并说明如何运行和验证。
测试环境有两个配置,写新测试时默认走前者:
npm test(对应 wdio.conf.ts):通过@wdio/static-server-service在localhost:8080起临时静态服务器,自包含,大多数测试开发应使用它。npm run test-mdbook(对应 wdio.conf-mdbook.ts):面向本地cargo xtask serve起的http://localhost:3000实例,只覆盖了baseUrl并清空services,仅用于对本地服务快速迭代。
准备条件
按 GEMINI.md 的项目约定:
# 1. 安装 Rust(rustup),克隆仓库后执行: cargo xtask install-toolscargo xtask install-tools会安装项目依赖的工具,包括固定的 nightly 工具链(nightly-2025-09-01)、mdbook 及其 Bazel 构建的预处理器插件。没有这一步,后续的构建与测试无法正常工作。
wdio.conf.ts中与稳定性相关的默认值(写测试时可依赖这些行为):
baseUrl: "http://localhost:8080",静态服务器挂载env.TEST_BOOK_DIR || "../book/html";waitforTimeout: 10000:所有waitFor*命令的默认超时;mochaOpts.timeout: 60000:单个 Mocha 测试的超时;- Chrome capabilities 默认只带
--headless;当CI环境变量存在时,会追加--disable-gpu、--no-sandbox、--disable-dev-shm-usage等标志以提升 CI 稳定性。
用 beforeEach 中的 browser.refresh() 消除测试间状态泄漏
redbox.test.ts中的 测试 展示了本仓库确认有效的做法。直接调用browser.url()并不能保证干净的起点:JavaScript 施加的行内样式等残留状态可能从一个测试带到下一个,造成难以解释的失败。redbox.test.ts的注释明确写着:按 WebdriverIO 文档这“本不应该必要,但没有它测试会失败”。完整的beforeEach如下:
beforeEach(async () => { await browser.url("/hello-world.html"); await browser.execute(() => sessionStorage.clear()); // Clear any lingering state (like inline styles) from previous // tests. Reading https://webdriver.io/docs/api/browser/url, // this should not be necessary, but tests fail without it. await browser.refresh(); });即导航到被测页面后,先清掉sessionStorage(该功能用showRedBox键持久化状态,不清会让“默认隐藏”“URL 参数覆盖”这类断言互相污染),再refresh()强制整页重载以清掉行内样式等残留状态。speaker-notes.test.ts 的beforeEach同样是browser.url(...)后紧跟browser.refresh(),而它的afterEach还负责关闭测试中打开的新窗口,避免句柄泄漏到后续测试:
afterEach(async () => { const handles = await browser.getWindowHandles(); if (handles.length > 1) { await browser.switchToWindow(handles[1]); await browser.closeWindow(); await browser.switchToWindow(handles[0]); } });用等待与自定义命令消除竞态条件
本页面的很多元素由 JavaScript 在初始加载后动态创建。如果测试在导航后立即访问,脚本可能还没跑完、元素尚不在 DOM 中。GEMINI.md 给出的规则是:在对动态元素交互或断言(如toBeDisplayed())之前,始终先await element.waitForExist()。
另一类竞态出现在模拟键盘等操作之后。wdio.conf.ts 的before钩子里注册了自定义命令toggleRedBox,注释说明原因:直接的browser.keys()调用“被证明是不稳定的,会造成间歇性测试失败”。该命令用waitUntil等待 UI 真正反映状态变化,从而消除竞态:
browser.addCommand("toggleRedBox", async function () { const redBox = await $("#aspect-ratio-helper"); const initialVisibility = await redBox.isDisplayed(); // Perform the toggle action. await browser.keys(["Control", "Alt", "b"]); // Wait until the visibility state has changed. await browser.waitUntil( async function () { const currentVisibility = await redBox.isDisplayed(); return currentVisibility !== initialVisibility; }, { timeout: 5000, timeoutMsg: `Red box display state did not toggle after 5s. Initial state: ${initialVisibility}`, }, ); });如果你的被测功能也有“操作后状态异步变化”的形态,照这个模式写自定义命令(操作 +waitUntil轮询目标状态),比在断言前硬编码等待更可靠。
导航一律使用最终的非重定向 URL
book.toml 在[output.html.redirect]段定义了一张重定向表,例如"async.html" = "concurrency/welcome-async.html",大量旧路径会 301 到新的课程结构。GEMINI.md 的警告是:导航到重定向 URL 虽然浏览器会跟随,但这个过程会丢掉 URL 查询参数,导致依赖参数的功能测试失败。所以写测试时导航目标一律用重定向表右值那侧的最终页面。redbox.test.ts里用/hello-world.html?show-red-box=true验证 URL 参数行为,就是针对最终 URL 写的;而book.toml中确实存在"welcome-day-1/what-is-rust.html" = "../hello-world/what-is-rust.html"这类条目,若导航到旧路径?show-red-box=true之类的参数就会在跳转中丢失。
运行测试与验证稳定性
在tests/目录下运行:
# 运行全部 spec(静态服务器模式,端口 8080) npm test # 只运行文件名匹配 redbox 的 spec npm test -- --spec redbox # 只运行文件内单个用例(Mocha grep) npm test -- --spec redbox --mochaOpts.grep "should be hidden by default" # 用重复执行检查偶发性 npm test -- --spec redbox --repeat 100仓库根目录下等价的入口是cargo xtask web-tests,可选--dir <book_html_directory>指定构建产物目录(它会同时刷新slide-style-guide.test.ts使用的幻灯片列表);本地快速迭代时也可以cargo xtask serve起 3000 端口服务后改用npm run test-mdbook。
验证标准:
npm test全部用例通过;- 用
--repeat(如 100 次)重复跑改动过的 spec,确认无偶发失败; - 在设置
CI环境变量的条件下跑一遍,确认追加的 Chrome 标志下依然通过。
区分功能失败与环境故障
按 tests/README.md 的说法,失败先分两类:
- 合理告警(Legitimate warnings):例如页面过长需要缩短或申请豁免,或 mdbook 基础设施的破坏性变更打断了功能。这类问题必须在合并前修复。
- 测试环境损坏(Broken test environment):CI 中出现如
ERROR webdriver: WebDriverError: tab crashed的报错时,可能不是你的改动导致的。此时应提交 bug 报告;若其余检查都通过且你确信改动正确,可以绕过 web-test 要求合并 pull request 作为临时措施。
新测试写完后的落点就是上面三件事:beforeEach里带refresh()的状态清理、动态元素先waitForExist()(异步状态用waitUntil封装成自定义命令)、导航只认最终 URL,最后用--repeat证明没有 flakiness 再提交。
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考