使用 WebdriverIO Browser Runner 进行组件测试原理、配置与实战指南【免费下载链接】webdriverioNext-gen browser and mobile automation test framework for Node.js项目地址: https://gitcode.com/GitHub_Trending/we/webdriverioWebdriverIO 的组件测试Component Testing依托其 Browser Runner让你能够在真实的桌面或移动浏览器中运行组件测试并通过 WebDriver 协议真实地驱动页面上的元素。本文将以 WebdriverIO 组件测试为唯一主线带你从初始化项目、理解 Vite Mocha WebDriver 的底层协作机制到配置 Test Harness、Setup Scripts、Mocking、覆盖率、Watch 调试与 Selenium Grid 集成全面掌握在真实浏览器中测试 React、Vue、Svelte、Lit、Stencil 等组件的完整方案。为什么在真实浏览器中测试组件与许多把测试跑在 JSDOM 上的测试框架不同WebdriverIO 的 Browser Runner 会把测试代码放进一个由 Vite 启动的真实浏览器页面中执行。这种做法的核心价值在于测试环境无限接近用户真实使用的环境而不是 Node.js 里对 DOM 标准的一种重实现。在 Runner 文档 中给出了 JSDOM 与 Browser Runner 的直观对比| | JSDOM | WebdriverIO Browser Runner | |-|-------|----------------------------| | 1. | 在 Node.js 中运行使用对 Web 标准尤其是 WHATWG DOM 与 HTML 标准的重实现 | 在真实浏览器中执行代码运行在用户实际使用的环境里 | | 2. | 与组件的交互只能用 JavaScript 模拟 | 可以通过 WebdriverIO API 借助 WebDriver 协议真实驱动元素 | | 3. | Canvas 支持需要额外的 canvas 依赖且有较多限制 | 直接使用真实的 Canvas API | | 4. | 存在一些 caveats 和不支持的 Web API | 所有 Web API 都受支持因为测试运行在真实浏览器中 | | 5. | 无法检测跨浏览器问题 | 支持所有浏览器包括移动端浏览器 | | 6. | 无法测试元素伪状态 | 支持:hover、:active等伪状态 |工作原理Vite 渲染 Mocha 执行 WebDriver 交互Browser Runner 的执行链路可以概括为三步Vite 渲染测试页面Runner 用 Vite 启动一个开发服务器把测试代码编译后加载到浏览器页面中Mocha 在浏览器内执行测试Runner 在浏览器页面中初始化测试框架运行测试。从 constants.ts 可以看到目前仅支持 MochaCurrently only mocha is supported as framework when using wdio/browser-runner.Jasmine 与 Cucumber 在 WebdriverIO 的路线图中即使你的项目本身不使用 Vite也可以用它来测试任何类型的组件WebDriver 交互Vite 服务器由 WebdriverIO testrunner 启动并按照普通 e2e 测试的配置接入所有 reporter 与服务。Runner 同时初始化一个browser实例让你访问 WebdriverIO API 中与页面元素交互的那一部分能力。和 e2e 测试一样你可以通过挂载在全局作用域上的browser变量访问它也可以根据injectGlobals的取值从wdio/globals导入。从源码可以更清楚地看到这条链路。在 packages/wdio-browser-runner/src/index.ts 中BrowserRunner.run()会先根据 headless/watch 选项修正 capabilities然后创建ViteServer并启动把 Vite 服务器的host:port作为baseUrl传给 workerpackages/wdio-browser-runner/src/vite/server.ts 中的ViteServer.start()则负责按 preset 加载框架插件、合并用户自定义的 Vite 配置、通过get-port自动分配空闲端口后调用createServer()启动。测试页面之间则通过隔离来保证互不干扰——Runner 文档说明每个测试文件或测试文件组运行在单个页面中每轮测试之间页面会被重新加载以隔离状态。在浏览器侧packages/wdio-browser-runner/src/browser/frameworks/mocha.ts 定义了一个 Shadow DOM 内的自定义元素MochaFramework它读取window.__wdioEnv__中的参数完成mocha.setup()并把 Mocha 的 HTML reporter 挂到自己的 Shadow Root 中从而把测试结果通过 worker 消息通道回传给 Node 侧的 testrunner。内置框架支持WebdriverIO 为以下前端框架提供了开箱即用的检测与配置Nuxttestrunner 检测到 Nuxt 应用后会自动配置好项目的 composables 并帮助你 mock 掉 Nuxt 后端详见 Vue 组件测试文档TailwindCSStestrunner 检测到项目使用 TailwindCSS 后会把环境正确加载到测试页面中。仓库中的示例配置可见 e2e/browser-runner/fixtures/tailwind.config.js。初始化项目从 wdio 配置向导开始在项目根目录执行以下任一命令创建一个用于单元测试或组件测试的 WebdriverIO 工程npm init wdiolatest ./ # 或 yarn create wdio ./配置向导启动后选择browser表示要运行单元测试和组件测试如果使用了上述组件框架可以挑选对应preset如react、vue、svelte等如果只想跑基础单元测试选Other即可如果项目里已经在用 Vite还可以直接配置一个自定义 Vite 配置供复用。完成后你会得到一份包含runner属性的wdio.conf.js。通过定义不同的 capabilities你还可以在不同浏览器中运行测试甚至并行执行。注意WebdriverIO 默认在 CI 中以无头模式headless运行浏览器测试即当CI环境变量被设为1或true时。你可以用 runner 的headless选项手动控制该行为。深入 Browser Runner 配置选项组件测试的配置核心是wdio.conf.js中的runner属性。它既可以写成字符串runner: browser也可以写成数组形式以传入选项// wdio.conf.js export const config { // ... runner: [browser, { // runner options }], // ... }以下是 Runner 文档中定义的常用选项。preset如果你使用 React、Preact、Vue、Svelte、SolidJS 或 Stencil 等框架测试组件可以定义 preset让一切开箱即用地配置好。注意它不能与viteConfig同时使用。类型vue|svelte|solid|react|preact|stencil示例export const config { // ... runner: [browser, { preset: svelte }], // ... }在源码层面ViteServer.start() 会根据preset从PRESET_DEPENDENCIES映射表找到对应的 Vite 插件并加载。viteConfig定义你自己的 Vite 配置。可以传入一个自定义对象也可以直接引用项目中已有的vite.conf.ts文件如果你用的是函数形式只有当值被读取时才会解析配置适合包含大量插件的场景。注意 WebdriverIO 会保留自定义 Vite 配置来搭建测试环境。类型string或UserConfig或(env: ConfigEnv) UserConfig | PromiseUserConfig示例import viteConfig from ../vite.config.ts export const config { // ... runner: [browser, { viteConfig }], // 或者直接给路径 runner: [browser, { viteConfig: ../vite.config.ts }], // 或者用函数延迟求值 runner: [browser, { viteConfig: () ({ // ... }) }], // ... }从 ViteServer 的合并逻辑 可以看到字符串形式会被import()动态导入后取其default函数形式会传入{ command, mode }环境后求值最终通过deepmerge与 WebdriverIO 自身的配置合并且自定义配置中的 plugins 会被追加在 runner 内置插件之前。headless设为true时runner 会更新 capabilities 以无头模式运行测试。默认值为false当CI环境变量被设为1或true时自动开启。rootDir项目根目录默认process.cwd()。仓库 e2e 测试中通过rootDir: path.resolve(__dirname, ..)指向项目根见 e2e/browser-runner/wdio.conf.js。coverage对象类型用于配置基于 istanbul 的覆盖率上报详见下文覆盖率上报一节。构造测试环境Test Harness测试中如何渲染组件完全由你决定官方推荐使用 Testing Library 作为工具库它为 React、Preact、Svelte、Vue 等主流组件框架都提供了插件非常适合把组件渲染到测试页面中并且会在每个测试结束后自动清理这些组件。你可以任意混合 Testing Library 的原语与 WebdriverIO 命令例如用render挂载组件、用$选择器与 WebdriverIO 命令做真实用户交互import { $, expect } from wdio/globals import { render } from testing-library/react import LoginForm from ./components/LoginForm describe(LoginForm, () { it(should call onLogin handler if username and password was provided, async () { render(LoginForm onLogin{onLogin} /) await $(input[nameusername]).setValue(testuser123) await $(input[namepassword]).setValue(s3cret) await browser.keys(Key.Enter) // ... }) })注意使用 Testing Library 的render方法有助于在测试之间移除已创建的组件。如果你不用 Testing Library请确保把测试组件挂载到一个会在测试间被清理的容器里。仓库 e2e 目录下提供了大量真实可运行的组件测试样例例如 React、Vue、Svelte、Lit 以及 Preact、Stencil 等对应的测试组件位于 e2e/browser-runner/components。以 Vue 为例完整的 Vue preset 配置与测试写法参见 Vue.mdReact 的写法参见 React.md。Setup Scripts在测试加载前后注入任意脚本你可以在测试前运行任意脚本Node.js 侧或浏览器侧例如注入样式、mock 浏览器 API 或连接第三方服务。规则是hooks如before在 Node.js 中运行mochaOpts.require允许你在测试加载前把脚本导入浏览器。例如下面的配置require指向一个浏览器侧 setup 脚本before钩子则在 Node.js 侧准备测试环境export const config { // ... mochaOpts: { ui: tdd, // 提供一个在浏览器中运行的 setup 脚本 require: ./__fixtures__/setup.js }, before: () { // 在 Node.js 中设置测试环境 } // ... }比如你想用下面的 setup 脚本 mock 掉测试中所有的fetch()调用import { fn } from wdio/browser-runner // 在所有测试加载前运行 window.fetch fn() export const mochaGlobalSetup () { // 在测试文件加载后运行 } export const mochaGlobalTeardown () { // 在 spec 文件执行完成后运行 }之后就可以在测试中为所有浏览器请求提供自定义的响应值了。mochaGlobalSetup/mochaGlobalTeardown属于 Mocha 的全局 fixtures 机制更多细节可参考 Mocha 官方文档。仓库中的真实实现见 e2e/browser-runner/fixtures/setup.js它通过window.globalSetupScriptExecuted true和window.mochaGlobalSetupExecuted true标记脚本是否执行对应 e2e 测试 lit.test.js 会断言这些标记。在 Node 侧BrowserRunner 构造函数 会把mochaOpts.require中的相对路径转换为基于rootDir的绝对文件 URL从而让浏览器能够正确加载。Mocking函数、模块与依赖编写测试时几乎总会需要创建内部或外部服务的假实现。WebdriverIO 提供了fn、spyOn、mock、unmock等工具函数可直接从wdio/browser-runner导入完整 API 见 API 文档完整教程见 mocking.md。fn创建一个 spy 函数mock用于跟踪其执行情况spyOn跟踪一个已存在对象上的某个方法。示例验证onLogin处理器是否被调用import { $, expect } from wdio/globals import { fn } from wdio/browser-runner import { Key } from webdriverio import { render } from testing-library/react import LoginForm from ../components/LoginForm describe(LoginForm, () { it(should call onLogin handler if username and password was provided, async () { const onLogin fn() render(LoginForm onLogin{onLogin} /) await $(input[nameusername]).setValue(testuser123) await $(input[namepassword]).setValue(s3cret) await browser.keys(Key.Enter) expect(onLogin).toBeCalledTimes(1) expect(onLogin).toBeCalledWith(expect.equal({ username: testuser123, password: s3cret })) }) })spyOn可以替换组件类原型上的方法配合mockReturnValue控制返回值import { expect, $ } from wdio/globals import { spyOn } from wdio/browser-runner import { html, render } from lit import { SimpleGreeting } from ./components/LitComponent.ts const getQuestionFn spyOn(SimpleGreeting.prototype, getQuestion) describe(Lit Component testing, () { it(should render with mocked component function, async () { getQuestionFn.mockReturnValue(Does this work?) render(htmlsimple-greeting nameWebdriverIO /, document.body) const innerElem await $(simple-greeting).$(p) expect(await innerElem.getText()).toBe(Hello, WebdriverIO! Does this work?) }) })WebdriverIO 在此直接重新导出了vitest/spy一个轻量、兼容 Jest 的 spy 实现可与 WebdriverIO 的expect匹配器一起使用。你也可以安装任何支持浏览器环境的 spy 框架如 SinonJS。模块 mock使用mock()覆盖模块导入。例如组件从utils.js导入handleClick就可以这样 mockimport { mock, fn } from wdio/browser-runner mock(./utils.ts, () ({ handleClick: fn() }))依赖 mock则用于避免真实调用外部 API例如 axios 请求让测试更快、更稳定import axios from axios import { mock, fn } from wdio/browser-runner import Users from ./users.js mock(axios, () ({ default: { get: fn() } })) describe(User API, () { it(should fetch users, async () { const users [{name: Bob}] const resp {data: users} axios.get.mockResolvedValue(resp) const data await Users.all() expect(data).toEqual(users) }) })Partial mock支持只 mock 模块的局部原始模块会被传入 mock 工厂函数因此可以只替换默认导出和部分命名导出同时保留其余实现。手动 mockManual Mocks则把 mock 实现放在__mocks__/子目录中Node 模块如lodash放在__mocks__/lodash.jsscoped 模块scope/project-name放在__mocks__/scope/project-name.js配合automock选项可在不显式调用mock()时也自动使用手动 mock此时可用unmock(moduleName)选择退出。由于 mock 需要在浏览器中生效WebdriverIO 会重写测试文件并把 mock 调用提升hoist到所有代码之上。这限制了变量传入 mock 解析器的方式——你不能引用文件级作用域里的变量必须把变量定义在 resolver 内部可改用await import(...)// ❌ 失败dep 和 variable 在 mock resolver 中未定义 mock(./some/module.ts, () ({ exportA: dep, exportB: variable })) // ✔️ 可行所有变量都在 resolver 内定义 mock(./some/module.ts, async () { const dep await import(dependency) const variable foobar return { exportA: dep, exportB: variable } })仓库 e2e 测试 lit.test.js 中完整覆盖了常量模块 mock、CJS 模块 mock、第三方库 mock、unmock与命名空间模块 mock 等多种场景。如果你想 mock 的是浏览器请求如 API 调用请参考 MocksAndSpies.md。覆盖率上报Browser Runner 支持基于 istanbul 的代码覆盖率上报testrunner 会自动对代码插桩并采集覆盖率。启用方式export const config { // ... runner: [browser, { preset: process.env.WDIO_PRESET, coverage: { enabled: true } }], // ... }完整的 coverage 选项见 Runner 文档 与 coverage.md选项类型默认值说明enabledbooleanfalse启用覆盖率采集includestring[][**]纳入覆盖率的 glob 文件列表excludestring[]一组内置排除项如dist/**、coverage/**、各类*.test.*文件从覆盖率中排除的 glob 文件列表extensionstring \| string[][.js, .cjs, .mjs, .ts, .mts, .cts, .tsx, .jsx, .vue, .svelte]报告应包含的文件扩展名reportsDirectorystring./coverage覆盖率报告输出目录reporterstring[][text, html, clover, json-summary]使用的 istanbul 报告器perFilebooleanfalse是否按文件检查阈值cleanbooleantrue运行测试前清理覆盖率结果lines/functions/branches/statementsnumberundefined行/函数/分支/语句阈值覆盖率报告的生成在 BrowserRunner.shutdown() 中完成_generateCoverageReports()合并所有 worker 的覆盖率 map通过istanbul-lib-report与istanbul-reports写出报告并根据阈值返回测试是否达标不达标时打印ERROR: Coverage for ... does not meet ... threshold。仓库 e2e 配置 e2e/browser-runner/wdio.conf.js 中coverage: { enabled: true, functions: 80 }就是一个真实用例。忽略代码如果希望故意把某些代码段排除在覆盖率之外可以在源码中使用以下解析提示/* istanbul ignore if */忽略下一个if语句/* istanbul ignore else */忽略if语句的else部分/* istanbul ignore next */忽略源码中下一个东西函数、if 语句、类等/* istanbul ignore file */忽略整个源文件应放在文件顶部。建议把测试文件排除在覆盖率报告之外否则可能引发错误例如调用execute或executeAsync命令时。如果一定要保留它们请确保对它们禁用插桩例如await browser.execute(/* istanbul ignore next */() { ... })。监听测试与应用文件变更调试浏览器测试有几种方式最简单的是给 WebdriverIO testrunner 加--watch标志启动npx wdio run ./wdio.conf.js --watch这会先完整运行所有测试并暂停。之后你改动任意文件对应的测试会被单独重新运行。如果你通过filesToWatch指向应用文件那么当应用代码发生变化时所有测试都会被重新运行。调试debug 命令与浏览器 DevTools虽然目前还无法让 IDE 中的断点被远程浏览器识别但你可以用debug命令在任意位置暂停测试从而打开 DevTools在 Sources 面板中设置断点进行调试。浏览器侧的 debug 实现见 packages/wdio-browser-runner/src/browser/commands/debug.ts它会打印Debug Mode Enabled并把window.wdioDebugContinue绑定为恢复执行的回调。当debug命令被调用时你的终端还会得到一个 Node.js REPL 界面提示如下The execution has stopped! You can now go into the browser or use the command line as REPL (To exit, press ^C again or type .exit)按Ctrl或 macOS 上的Commandc或输入.exit即可继续执行测试。通过 Selenium Grid 运行如果你已经部署了 Selenium Grid并希望通过 Grid 来跑浏览器就必须设置hostrunner 选项让浏览器能够访问承载测试文件的正确主机例如export const config: WebdriverIO.Config { runner: [browser, { // 运行 WebdriverIO 进程的那台机器的网络 IP host: http://172.168.0.2 }] }这能确保浏览器正确打开运行 WebdriverIO 测试的实例所托管的服务器实例。在源码层面BrowserRunner.run() 使用this.#options.host || DEFAULT_HOST拼接出baseUrl并将它写入 worker 参数Firefox 场景下还会额外注入wdio:geckodriverOptions.allowOrigins以便 Geckodriver 接受来自浏览器的 Bidi 消息。已知局限使用 Browser Runner 时需要注意alert、confirm这类阻塞线程的对话框无法原生使用因为它们会阻塞网页导致 WebdriverIO 无法继续与页面通信而使执行挂起。WebdriverIO 为这些 API 提供了返回默认值的默认 mock确保即使你不小心用到了同步弹窗 Web API 也不会挂起。不过官方仍建议你自行 mock 这些 Web API 以获得更好体验详见 mocking.md。小结WebdriverIO 组件测试的核心价值在于把测试搬进真实浏览器Vite 负责渲染测试页面Mocha 在浏览器内执行用例WebDriver 协议驱动元素交互同时完整复用 testrunner 的 reporter、service 与配置体系。本文覆盖了从npm init wdio初始化、preset/viteConfig/headless/coverage等 runner 选项、Testing Library Test Harness、mochaOpts.requireSetup Scripts、fn/spyOn/mockmocking 工具、istanbul 覆盖率到--watch监听、debug调试与 Selenium Grid 集成的完整实战链路。如果你想深入更多框架的示例仓库的 e2e/browser-runner 目录下还有 React、Vue、Svelte、Lit、Preact、Stencil 等组件的真实测试文件可以直接阅读参考。【免费下载链接】webdriverioNext-gen browser and mobile automation test framework for Node.js项目地址: https://gitcode.com/GitHub_Trending/we/webdriverio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
