深入 WebdriverIO 自定义 Reporter 开发指南:基于 `@wdio/reporter` 构建事件驱动的测试报告器 📅 发布时间:2026/9/15 14:27:43 👁 浏览次数: 深入 WebdriverIO 自定义 Reporter 开发指南基于wdio/reporter构建事件驱动的测试报告器【免费下载链接】webdriverioNext-gen browser and mobile automation test framework for Node.js项目地址: https://gitcode.com/GitHub_Trending/we/webdriveriowdio/reporter是 WebdriverIO 生态中面向「报告器Reporter」的基础工具包它把测试运行期间的 runner、suite、hook、test 以及客户端命令等各类事件统一规范化并以一致的Stats数据结构暴露给开发者。本指南以 packages/wdio-reporter/README.md 为骨架结合仓库源码讲解如何从零实现一个可发布到 NPM 的自定义 Reporter掌握基类继承、事件监听、选项注入、日志输出、同步控制与各事件数据结构的完整细节。背景Reporter 在 WebdriverIO 中的角色WebdriverIO 的测试运行器testrunner在运行期间会产生大量事件从runner:start、suite:start、test:start到client:beforeCommand等。官方自带 dot、spec、junit、allure、json 等报告器但真实项目常常需要把测试结果发送到自建平台、云日志或内部看板。此时wdio/reporter就是官方提供的「脚手架」你只需继承它的WDIOReporter基类源码中导出为WDIOReporter见 packages/wdio-reporter/src/index.ts按约定实现若干on*回调即可获得一套统一、完整、与框架Mocha/Jasmine/Cucumber无关的事件数据。从源码结构看该包由三部分协作组成基类WDIOReporterindex.ts继承EventEmitter在构造时订阅所有框架事件并把它们转换为各类Stats对象再分发到on*钩子五个 Stats 模型src/stats 目录RunnableStats、RunnerStats、SuiteStats、HookStats、TestStats分别描述可执行对象统一计时、测试运行实例、套件、钩子与测试用例工具函数src/utils.ts提供 capability 清洗、错误抽取、断言 diff 格式化、颜色输出等能力。快速上手安装与最小实现把wdio/reporter作为自定义报告器的依赖安装npm install wdio/reporter或使用 yarnyarn add wdio/reporter随后新建一个文件继承WDIOReporterREADME 中将其称为Reporter二者是同一基类的不同叫法实现你关心的回调import Reporter from wdio/reporter export default class MyCustomReporter extends Reporter { constructor () { super() // your custom logic if necessary // ... } onRunnerStart () { // ... } onSuiteStart (suite) { // ... } // ... }基类会在事件被触发时调用你实现的on*函数并以统一格式提供事件信息。如果你不想走on*回调、希望拿到框架抛出的原始数据也可以在构造函数里直接注册事件监听import Reporter from wdio/reporter export default class MyRawReporter extends Reporter { constructor () { super() this.on(suite:start, (raw) { // 直接拿到框架层原始事件数据 }) } }基类构造时注册的原始事件监听器见 index.ts覆盖了client:beforeCommand、client:afterCommand、client:beforeAssertion、client:afterAssertion、runner:start、suite:start、suite:retry、hook:start、hook:end、test:start、test:pass、test:skip、test:fail、test:retry、test:pending、test:end、suite:end、runner:end等事件并把它们分别转换为对应的Stats对象后分发给on*空实现默认均为空操作见 index.ts。配置this.options的注入、合并与覆盖用户可以在wdio.conf.js中为每个 reporter 传入自定义配置。默认情况下WebdriverIO 会向 reporter 注入outputDir与logLevel两个选项用户也可以覆盖它们。例如配置文件如下// wdio.conf.js exports.config { // ... reporters: [dot, [my-reporter, { outputDir: /some/path, foo: bar }]] // ... }那么在自定义 reporter 类里拿到的this.options就是合并后的结果export default class MyReporter extends Reporter { constructor () { super() console.log(this.options) /** * outputs: * { * outputDir: /some/path, * logLevel: trace, * foo: bar * } */ } }从基类签名constructor(public options: PartialReporters.Options)index.ts可以看出构造器接收的就是报告器选项对象。选项除了注入的outputDir、logLevel外还与日志输出目标密切相关基类在构造时依据选项决定输出流index.ts若设置了outputDir会先递归创建报告目录fs.mkdirSync(..., { recursive: true })失败则抛出带堆栈的错误输出流的选取逻辑为当stdout为真、或未指定logFile时优先使用传入的writeStream写标准输出否则创建一个指向logFile的写入流fs.createWriteStream。因此你完全可以用stdout: true/false与logFile来控制日志去向而无需自己管理文件句柄。用write方法输出日志无论目标是标准输出还是日志文件都请使用基类提供的内部方法write来推送日志例如class MyReporter extends Reporter { constructor (options) { /** * make dot reporter to write to output stream by default */ options Object.assign(options, { stdout: true }) super(options) } // ... onTestPass (test) { this.write(test ${test.title} passed) } // ... }输出效果如下MyReporter Reporter: test some test passed test some other test passed spec Reporter: ...write的实现index.ts还会维护一个isContentPresent标志一旦写过非空内容就置为true。这个标志用于runner:end时清理空日志——如果整个运行期间该 reporter 没有写出任何内容isContentPresent为 false且配置了logFile基类会在结束时删除这个空日志文件index.ts。同步用isSynchronised控制进程退出时机如果 reporter 需要在测试结束后做异步计算比如把日志上传到远程服务器可以覆写isSynchronisedgetter 来管理这一过程。默认实现恒返回trueindex.ts因为多数 reporter 不需要异步工作。需要时这样覆写class MyReporter extends Reporter { constructor (options) { // ... } get isSynchronised (test) { return this.unsyncedMessages.length 0 } // ... }WebdriverIO 的 testrunner 会一直等待直到每个 reporter 的isSynchronised都为true才杀死 runner 进程。仓库里 packages/wdio-sumologic-reporter/src/index.ts 是 README 明确提及的权威示例它内部维护一个_unsynced字符串数组isSynchronised直接返回this._unsynced.length 0第 68-70 行。所有onRunnerStart、onTestPass、onTestFail等回调只是把事件序列化后 push 进_unsynced真正的上传逻辑放在sync()方法里按syncInterval默认 100ms定时执行每次最多取前MAX_LINES 100行通过fetchPOST 到sourceAddress成功后从桶中splice掉已发送的行失败则按指数退避_retryDelay最大封顶MAX_RETRY_DELAY 1000ms并在runner:end后清空队列时clearInterval。这套「unsynced 队列 定时同步 指数退避」的模式正是异步上报类 reporter 的标准写法。事件全景框架无关的Stats数据结构在一次测试运行中WebdriverIO 会抛出一系列事件均可通过你的回调函数捕获。基类把事件统一包装成SuiteStats、HookStats、TestStats、RunnerStats四类对象它们全部继承自RunnableStats保证「无论跑的是哪个框架数据结构一致」。RunnableStatssrc/stats/runnable.ts负责统一计时记录start、end通过complete()计算_duration并提供一个durationgetter——运行中返回「当前时刻 − 开始时刻」的实时耗时结束后返回固定值。测试事件Test Events这些事件包含与测试相关的数据与具体框架无关。onSuiteStartSuiteStats { type: suite, start: 2018-02-09T13:30:40.177Z, duration: 0, uid: root suite2, cid: 0-0, title: root suite, fullTitle: root suite, tests: [], hooks: [], suites: [] } }onSuiteEndSuiteStats { type: suite, start: 2018-02-09T13:30:40.177Z, duration: 1432, uid: root suite2, cid: 0-0, title: root suite, fullTitle: root suite, tests: [ [TestStats] ], hooks: [ [HookStats], [HookStats] ], suites: [ [Object] ], end: 2018-02-09T13:30:41.609Z } }结合 src/stats/suite.ts 的实现可见SuiteStats会随着事件不断累积tests、hooks、suites是三个数组分别挂在当前套件下还维护了一个hooksAndTests数组按实际发生顺序混排该套件内的钩子与测试——这对需要还原执行顺序的报告器很有用。基类内部用currentSuites栈维护嵌套关系suite:start时新建SuiteStatspush 进当前套件再入栈suite:end时complete()并弹栈[index.ts](https://link.gitcode.com/i/15e05288f82f5487a3488ab988701571#L77-L95, L209-L214)。uid由RunnableStats.getIdentifier生成优先用事件自带的uid否则退回title。onSuiteRetry仅 CucumberSuiteStats { start: 2024-11-13T06:33:21.499Z, end: undefined, type: scenario, uid: 0, cid: 0-0, file: /Users/christian.bromann/Sites/WebdriverIO/projects/webdriverio/examples/wdio/cucumber/features/my-feature.feature, title: Get size of an element, fullTitle: my-feature.feature:1:1: Get size of an element, tags: [], tests: [], hooks: [], suites: [], parent: my-feature.feature:1:1, retries: 1, hooksAndTests: [], description: , rule: undefined }onSuiteRetry目前只在 Cucumber 中触发当某个套件被重试时框架发出suite:retry事件替代一次新的suite:start。基类对它的处理是取出已存在的SuiteStats并调用retry()index.ts。SuiteStats.retry()src/stats/suite.ts会自增retries计数并清空该套件此前累积的tests、hooks、hooksAndTests从而让报告器能够展示套件被重试的次数并避免旧数据残留。onHookStartHookStats { type: hook, start: 2018-02-09T13:30:40.181Z, duration: 0, uid: before each hook4, cid: 0-0, title: before each hook, parent: root suite,onHookEndHookStats { type: hook, start: 2018-02-09T13:30:40.181Z, duration: 1, uid: before each hook4, cid: 0-0, title: before each hook, parent: root suite, end: 2018-02-09T13:30:40.182Z } }HookStatssrc/stats/hook.ts额外记录currentTest与 Mocha 专属的body其complete(errors?)方法会记录错误若钩子出错则把errors、error设为第一个错误并置state failed否则标记为通过。onTestStartTestStats { type: test, start: 2018-02-09T13:30:40.180Z, duration: 0, uid: passing test3, cid: 0-0, title: passing test, fullTitle: passing test, retries: 0, state: pending } }注意TestStats的初始state是pendingsrc/stats/test.ts之后可能流转为passed、skipped、failed。Cucumber 数据表Cucumber 测试会额外带一个argument属性包含 feature 文件中定义的数据表Data Table例如TestStats { type: test, start: 2019-07-08T08:44:56.666Z, duration: 0, uid: I add the following grocieries16, cid: 0-0, title: I add the following grocieries, output: [], argument: [{ rows: [{ cells: [Item, Amount], locations: [{ line: 17, column: 11 }, { line: 17, column: 24 }] }, { cells: [Milk, 2], locations: [{ line: 18, column: 11 }, { line: 18, column: 24 }] }, { cells: [Butter, 1], locations: [{ line: 19, column: 11 }, { line: 19, column: 24 }] }, { cells: [Noodles, 1], locations: [{ line: 20, column: 11 }, { line: 20, column: 24 }] }, { cells: [Schocolate, 3], locations: [{ line: 21, column: 11 }, { line: 21, column: 24 }] }] }], state: pending }Argument的类型定义在 src/types.tsTestStats.argument承载该数据src/stats/test.ts。行内还带有每个单元格的行列位置line/column便于精确定位 feature 文件中的来源。onTestSkipTestStats { type: test, start: 2018-02-09T14:01:04.573Z, duration: 0, uid: skipped test6, cid: 0-0, title: skipped test, fullTitle: skipped test, retries: 0, state: skipped }TestStats.skip(reason)src/stats/test.ts会把state置为skipped并记录pendingReason。基类还会把计数器的skipping加一。onTestPassTestStats { type: test, start: 2018-02-09T14:11:28.075Z, duration: 1503, uid: passing test3, cid: 0-0, title: passing test, fullTitle: passing test, retries: 0, state: passed, end: 2018-02-09T14:11:29.578Z } }pass()会调用complete()结束计时并把state置为passedsrc/stats/test.ts。onTestRetryTestStats { type: test, start: 2020-09-23T07:54:34.601Z, _duration: 2495, uid: test-00-0, cid: 0-0, title: test fails and retries, fullTitle: test fails and retries, retries: 0, state: failed, end: 2020-09-23T07:54:37.096Z, error: { message: some error, stack: Error: some error\n at Context.it (/path/to/project/test/b.js:17:19)\n at /path/to/project/packages/wdio-sync/src/index.js:490:28\n at Promise (anonymous)\ n at F (/path/to/project/node_modules/core-js/library/modules/_export.js:35:28)\n at Context.executeSync (/path/to/project/packages/wdio-sync/src/index.js:488:12)\n at /path/to/project/packages/wdio-sync/src/index.js:623:33, type: Error } } }onTestRetry是测试失败后进入重试时触发基类记录失败并把内部retries计数器加一index.ts下一次test:start时会把当前重试次数写入test.retries。onTestFailTestStats { type: test, start: 2018-02-09T14:11:29.581Z, duration: 21, uid: failing test8, cid: 0-0, title: failing test, fullTitle: failing test, retries: 0, state: failed, end: 2018-02-09T14:11:29.602Z, error: { message: some error, stack: Error: some error\n at Context.it (/path/to/project/test/b.js:17:19)\n at /path/to/project/packages/wdio-sync/src/index.js:490:28\n at Promise (anonymous)\ n at F (/path/to/project/node_modules/core-js/library/modules/_export.js:35:28)\n at Context.executeSync (/path/to/project/packages/wdio-sync/src/index.js:488:12)\n at /path/to/project/packages/wdio-sync/src/index.js:623:33, type: Error } } }fail()src/stats/test.ts在完成计时与置statefailed之外还做了断言错误格式化逐个检查错误对象若带expected/actual属性且未被框架预格式化则调用_stringifyDiffObjs用diffWordsWithSpace生成「expected 与 actual」的词级差异加上行号、颜色标记与图例后拼接到错误消息中见 src/stats/test.ts。this.errors保存全部错误this.error指向第一个。基类在test:fail时通过getErrorsFromEvent从事件里抽取错误数组src/utils.ts——该工具函数兼容 Jasmine 的软断言模型errors数组与 Mocha 的硬断言模型单个error。onTestEndTestStats { type: test, start: 2018-02-09T14:11:28.075Z, duration: 1503, uid: passing test3, cid: 0-0, title: passing test, fullTitle: passing test, retries: 0, state: passed, end: 2018-02-09T14:11:29.578Z } }test:end是测试的最终收尾事件基类在此时重置retries计数index.ts。Runner 事件Runner EventsRunner 事件描述一次测试运行实例一个或多个 spec 文件的执行的整体信息。onRunnerStartRunnerStats { type: runner, start: 2018-02-09T14:30:19.871Z, duration: 0, cid: 0-0, capabilities: { acceptInsecureCerts: false, browserName: firefox, browserVersion: 59.0, moz:accessibilityChecks: false, moz:headless: false, moz:processID: 92113, moz:profile: /var/folders/ns/8mj2mh0x27b_gsdddy1knnsm0000gn/T/rust_mozprofile.jlpfs632Becb, moz:webdriverClick: true, pageLoadStrategy: normal, platformName: darwin, platformVersion: 17.3.0, rotatable: false, timeouts: { implicit: 0, pageLoad: 300000, script: 30000 } }, sanitizedCapabilities: firefox.59_0.darwin, config: [Object], specs: [ /path/to/project/test/my.test.js ] }, retry: 0RunnerStatssrc/stats/runner.ts在runner:start时由基类构造它保存cid、完整capabilities、清洗后的sanitizedCapabilities由sanitizeCaps生成、config、specs、sessionId、isMultiremote与instanceOptions。注意sanitizedCapabilities字段sanitizeCapssrc/utils.ts会针对移动端appium:deviceName等与桌面端browserName/browserVersion/platformName分别拼接一个类似firefox.59_0.darwin的字符串常被用来生成文件名或展示友好的运行摘要。onRunnerEndRunnerStats { type: runner, start: 2018-02-09T14:30:19.871Z, duration: 1546, uid: undefined, cid: 0-0, capabilities: [Object], sanitizedCapabilities: firefox.59_0.darwin, config: [Object], specs: [ /path/to/project/test/my.test.js ], failures: 1, retries: 1, end: 2018-02-09T14:30:21.417Z } }onRunnerEnd时基类把运行器上报的failures、retries、error回填进RunnerStats并调用complete()index.ts随后执行上文提到的空日志文件清理逻辑。客户端事件Client Events客户端事件在「与自动化驱动交互」时触发描述具体的 WebDriver/DevTools 协议调用。它们不是 Stats 对象而是原始载荷对象因此建议用this.on(client:beforeCommand, ...)等方式直接监听。onBeforeCommand{ method: GET, endpoint: /session/:sessionId/element, body: { using: css selector, value: img } cid: 0-0, sessionId: 4d1707ae-820f-1645-8485-5a820b2a40da, capabilities: [Object] }onAfterCommand{ method: GET, endpoint: /session/:sessionId/element/fbf57b79-6521-7d49-b3b7-df91cf2c347a/rect, body: {}, result: { value: { x: 75, y: 11, width: 160, height: 160 } }, cid: 0-0, sessionId: 4d1707ae-820f-1645-8485-5a820b2a40da, capabilities: [Object] }这两个事件对应的参数类型定义在 src/types.tsBeforeCommandArgs在CommandArgs基础上增加bodyAfterCommandArgs增加result。基类在内部监听这两个事件index.ts并把它们写入currentTest.output数组分别标记type: command与type: result这就是TestStats.output中命令记录的来源。对带script的execute类命令基类会先调用transformCommandScriptsrc/utils.ts把冗长的脚本压缩成类似script fn myFn(...) [123 bytes]的摘要再入队避免output无限膨胀基类对外暴露的onBeforeCommand/onAfterCommand回调则由client:beforeCommand/client:afterCommand的注册顺序先于 payload 写入因此你可以在子类中自行处理原始载荷。此外还有onBeforeAssertion/onAfterAssertion两个断言钩子默认空实现对应client:beforeAssertion/client:afterAssertion事件。计数与数据累积基类内部状态一览基类在运行期间维护了一组公开字段供子类直接读取index.ts字段类型说明outputStreamWriteStream \| CustomWriteStream当前日志输出流stdout 或 logFile 流failuresnumber失败计数初始 0test:fail时递增suites/hooks/testsRecordstring, ...Stats以uid为键的统计对象索引currentSuitesSuiteStats[]套件栈栈顶即当前套件counts对象suites/tests/hooks/passes/skipping/failures/pending七个计数retriesnumber当前测试累计重试次数runnerStatRunnerStats?本次运行的 Runner 统计specs/currentSpecstring[]/string?spec 文件队列与当前 spec其中counts在test:pass、test:skip、test:fail、test:pending等处理分支中持续累加index.ts是自定义报告器输出「通过/失败/跳过/待定」汇总时最直接的依据。另外基类在构造时还会创建一个(root)根套件并推入currentSuites栈Jasmine 这类不提供文件信息的框架其 spec 文件名会通过this.specs.shift()从上到下匹配注入index.ts。完整实战一个带计数的命令行报告器把上述知识整合成一个可在wdio.conf.js中直接使用的自定义报告器示例import Reporter, { TestStats, SuiteStats, RunnerStats } from wdio/reporter export default class SummaryReporter extends Reporter { constructor (options) { options Object.assign(options, { stdout: true }) super(options) } onRunnerStart (runner) { this.write(Runner ${runner.cid} starting, spec: ${runner.specs.join(, )}\n) } onSuiteStart (suite) { this.write( Suite: ${suite.title}\n) } onTestPass (test) { this.write( ✓ ${test.title} (${test.duration}ms)\n) } onTestFail (test) { this.write( ✗ ${test.title}: ${test.error test.error.message}\n) } onTestSkip (test) { this.write( - ${test.title} skipped\n) } onRunnerEnd (runner) { const c this.counts this.write(\nSummary: ${c.passes} passed, ${c.failures} failed, ${c.skipping} skipped, ${c.pending} pending\n) } }在wdio.conf.js中启用它export const config { reporters: [ spec, [summary, { stdout: true }] ] }基类同时导出了SuiteStats、HookStats、TestStats、RunnerStats以及getBrowserName等辅助工具index.ts需要强类型时可以直接引入这些类型。若要在 TypeScript 下扩展全局 reporter 选项类型可以参考wdio-sumologic-reporter的做法声明declare global { namespace WebdriverIO { interface ReporterOption extends Options {} } }见 packages/wdio-sumologic-reporter/src/index.ts。小结wdio/reporter的核心设计是「一次事件统一结构」基类作为EventEmitter把各框架的事件翻译成RunnerStats/SuiteStats/HookStats/TestStats四类模型并驱动on*回调子类只需聚焦业务逻辑打印、存储或上传。开发自定义报告器时把握四条主线即可继承与注册继承WDIOReporter用on*回调或this.on(...)订阅事件配置注入通过this.options读取用户配置与注入的outputDir/logLevel用this.options.stdout、this.options.logFile配合write()控制日志去向异步安全必要时覆写isSynchronised并参考 wdio-sumologic-reporter 的「待同步队列 定时重试」实现避免 runner 进程在数据上传前被提前结束数据利用优先使用counts与各类Stats的结构化字段处理断言 diff、Cucumber 数据表、client:beforeCommand/afterCommand等特殊载荷。如果想在仓库里继续深入建议阅读 packages/wdio-reporter/tests 目录下的测试用例来验证各类Stats在真实事件流中的行为或对比 packages/wdio-spec-reporter、packages/wdio-dot-reporter、packages/wdio-junit-reporter 等官方报告器如何基于这套基础框架落地。【免费下载链接】webdriverioNext-gen browser and mobile automation test framework for Node.js项目地址: https://gitcode.com/GitHub_Trending/we/webdriverio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考