尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flutter 测试覆盖率工具 test_cov 的鸿蒙适配实践

Flutter 测试覆盖率工具 test_cov 的鸿蒙适配实践 刚把公司里的 Flutter 业务搬到鸿蒙生态上时我最头疼的不是页面适配而是测试覆盖率统计——这套原来在 Android、iOS 上点一下就能跑完的东西换到鸿蒙上直接哑火。test_cov 这个三方库可能不少 Flutter 团队都在用它本质上是 Dart 单元测试覆盖率的实用工具支持 lcov 报告和自定义阈值是很多项目质量门禁里不可或缺的一环。可一进鸿蒙工程连不上服务、路径错乱、报告空白这些问题就接连冒出来。这篇文章是我做完鸿蒙化适配之后的完整记录不会只扔给你一堆命令而是把 test_cov 的工作机制、鸿蒙环境带来的差异、每一步怎么验证以及我实际踩过的坑都讲清楚。如果你正打算把 Flutter 测试体系迁到鸿蒙设备上或者已经在适配中卡壳了这篇内容应该能帮你省下不少排查时间。1. 先看 test_cov 的最短工作链路鸿蒙化就是对这条链路做手术1.1 test_cov 到底干了什么事test_cov 是 Dart 生态里的覆盖率统计 CLI 工具它的整体流程可以拆成四步让测试代码跑在带覆盖率采样的 Dart VM 上通过 VM Service 协议读取覆盖率数据把 VM Service 返回的 CodeCoverage JSON 转换成 LCOV 格式最后调用 lcov 工具生成 HTML 报告或者按最小覆盖率阈值做质量门禁。用一个生活类比来理解Dart VM 像是一间装了监控的办公室test_cov 就是那个查监控的人。测试用例每执行一行代码VM 内部的计数器就会更新一次。测试跑完test_cov 去问 VM“刚才哪些行被执行过”VM 返回一张表格test_cov 再把这张表格转换成大家通用的 lcov.info 文件最后交给 genhtml 渲染成网页。这里有个关键认知Dart 的覆盖率不是静态代码分析得出来的而是运行时采样出来的。也就是说它依赖 JIT 模式下 VM 的计数能力。release 模式的 AOT 编译产物不会保留覆盖率计数点所以在鸿蒙上如果拿 release 包跑集成测试拿到的覆盖率要么是 0要么根本采不到数据。这个前提一定要在动手前建立起来否则后面所有操作都是白费。1.2 鸿蒙环境对这条链路的三次冲击把 test_cov 搬到鸿蒙生态以后原来在开发机上默认成立的假设几乎全都不成立了主要冲击有三个。第一次冲击是 VM Service 的连接方式变了。在标准 Flutter 开发机上test_cov 启动测试进程后本地回环地址就能连上 VM Service。但在鸿蒙真机或模拟器上测试进程跑在设备里VM Service 监听的是设备内部端口。开发机怎么找到这个端口设备上有没有开启无线调试端口能不能转发这些都需要额外处理。很多人在这一步就卡住了报错大多是连接不上 VM Service。第二次冲击是覆盖率文件里的源代码路径发生了语义漂移。Dart VM 返回的脚本路径在鸿蒙上通常是以/data/app/或/data/storage/开头的设备路径指向安装包里的产物而不是开发机仓库里的源码路径。lcov 报告里的 SF 字段如果直接写设备路径genhtml 在本地根本找不到对应源码生成的报告全是空的。这个问题如果不做路径重映射后面报告出不来阈值再低也白搭。第三次冲击是依赖树的兼容性。test_cov 本身依赖 coverage、vm_service、path 等纯 Dart 包这些包在鸿蒙上通常能正常解析安装。真正的隐患是项目里那些直接依赖鸿蒙原生能力的插件比如通过 ohos 目录或 .har 包引入的模块。当 test_cov 在开发机上尝试加载整个测试入口时只要有一处 import 依赖 native 能力整个测试进程就可能起不来。所以我说鸿蒙化适配不是改 test_cov 的源码而是要把这三条链路重新接通。听起来不复杂实际操作里每一步都可能卡壳。1.3 需要准备的四个核心物料在动手之前先确认下面几样东西就位Flutter SDK、DevEco Studio、鸿蒙设备或模拟器以及一个能在 DevEco Studio 里正常构建运行的鸿蒙 Flutter 工程。Flutter SDK 不是版本越高越好而是必须和鸿蒙侧的 Flutter 引擎版本对齐。DevEco Studio 用来构建工程和查看设备日志。设备方面模拟器也可以跑集成测试但真机对 VM Service 端口转发的表现更接近生产环境建议至少准备一台。最容易被忽略的是最后一个“能正常运行的鸿蒙 Flutter 工程”如果基础工程都跑不起来那覆盖率适配根本无从谈起。很多项目卡在“test_cov 怎么都配不好”上回头才发现是 SDK 版本和 DevEco 构建工具不匹配。测试适配这种事情环境没对齐问题永远查不完。2. 适配前的装备对齐Flutter SDK、DevEco Studio 与测试基线选择2.1 版本对齐其实是“引擎对齐”在鸿蒙生态里Flutter 的发行渠道和标准 Flutter 不完全一样。OpenHarmony 社区和华为维护了对应的 Flutter SDK 分支。如果项目原来用的是标准 Flutter SDK直接切到鸿蒙分支最好保持版本号一致或者说 Dart 语言版本、引擎版本保持一致。为什么这么强调版本因为 test_cov 依赖的 coverage 包是通过 VM Service 协议读取数据的协议版本如果和引擎不匹配轻则拿到空数据重则握手失败。我遇到过一种情况SDK 用的是自己编译的版本但工程构建用的引擎包从 DevEco 侧下载两者并不完全一致测试一跑就协议报错。建议的操作顺序是先用flutter doctor确认整个工具链可用再用flutter --version记录 Flutter 和 Dart 的准确版本最后在 pubspec.yaml 里把 coverage、test_cov 选成和 Dart 版本兼容的版本范围不要无脑 latest。这步花不了十分钟但能避免后面大量无效排查。2.2 最小工程复现先确定失败类型适配工作切忌一上来就铺全量工程。单独建一个最简鸿蒙 Flutter 工程只包含一个待测的纯 Dart 工具类、一个对应的 test 文件再在 pubspec.yaml 里引入 test_cov这样能把干扰项全部隔离掉。如果这个最小工程能直接跑出覆盖率统计说明是业务工程里某些依赖破坏了链路如果连最小工程都跑不通说明是基础环境问题。这个二分法能帮你节省至少半天的时间。在做最小工程验证时先跑常规测试命令确保flutter test本身没问题再引入 test_cov 或配合 coverage 包做覆盖率收集。我强烈建议把“失败复现”固化成一个脚本每次改动依赖后重跑一遍快速定位是哪个环节坏了。2.3 区分两种测试形态适配策略完全不同这一步一定要讲清楚因为很多人就是在这里把方向搞错的。第一种是纯 Dart/Flutter 单元测试跑在开发机上不依赖鸿蒙设备。这种情况下 test_cov 的适配工作相对简单主要解决依赖冲突和路径归一化。比如在开发机上跑dart run test_cov如果项目里存在只支持鸿蒙平台的插件就要在测试中把相关插件 mock 掉或者用 test_cov 的 exclude 参数把这些插件关联的源文件排除出覆盖率统计。第二种是鸿蒙设备上的集成测试比如用 integration_test 驱动真实页面、调用鸿蒙原生能力。这种情况下测试进程跑在设备上VM Service 也在设备上必须先打通 VM Service 地址和端口转发再把设备路径映射回仓库最后才能生成有效报告。我见过不少人拿着“开发机上的单测脚本”要求“统计设备端集成测试覆盖率”这两者根本不是一回事。如果项目两种都需要就分别设计两条采集链路各自出报告不要混在一个脚本里。3. 第一道坎test_cov 连不上鸿蒙设备上的 VM Service解决办法3.1 覆盖率数据的入口VM Service 协议要理解连接失败的原因先要知道 test_cov 是怎么找到 VM Service 的。在标准 Flutter 开发机流程下test_cov 启动测试时会从测试进程的启动参数里拿到 VM Service 的 Uri通常是http://127.0.0.1:port/这种格式。但在鸿蒙设备上测试进程运行在设备内部它监听的是设备回环地址而不是开发机的地址。如果应用跑在模拟器里有些情况下模拟器端口做了默认转发看起来像是能直连其实绕了一层。到了真机上情况更复杂开发机既没有设备回环地址的路由也没有自动建立端口转发。所以第一件事就是确认 VM Service 到底监听到了哪里而不是盯着连接失败日志干瞪眼。3.2 三个可靠的手段拿到 VM Service 地址第一个手段翻日志。Flutter 引擎在调试模式启动时通常会在日志里输出 VM Service 的监听地址。鸿蒙上可以在 DevEco Studio 的日志窗口查看或者通过 hdc 命令抓取日志搜索关键词 “VM Service is listening on” 或 “Dart VM service is listening”。把这段日志保存下来里面的 ws 或 http Uri 就是入口。这个方式最直接不需要改任何代码。第二个手段在代码里主动输出。在测试入口文件里写一段 Dart 代码用 dart:developer 的 Service.getInfo() 拿到当前服务地址然后打印到日志里。这个方法对集成测试特别适用因为集成测试进程就是应用进程你可以在 setUpAll 里打印一次地址。示例代码如下import dart:developer; Futurevoid printVmServiceUri() async { final info await Service.getInfo(); final uri info.serverUri; print(VM_SERVICE_URI$uri); }第三个手段固定端口并用参数启动。部分引擎版本支持在启动参数里指定 VM Service 端口这样地址可控。不过在鸿蒙侧这个参数是否生效取决于 Flutter 版本和构建方式建议把它当作辅助手段不要依赖它作为唯一方案。3.3 端口转发开发机访问设备 VM Service 的桥拿到地址只是第一步。开发机要访问设备内部的端口通常需要建立一条隧道类似安卓开发里的端口反向转发。鸿蒙的调试链路使用 hdc 工具常见做法是端口转发。比如设备内部 VM Service 监听的是 12345 端口可以在开发机执行hdc fport tcp:9000 tcp:12345这条命令的含义是把开发机的 9000 端口转发到设备的 12345 端口。之后开发机上的测试脚本就可以访问http://127.0.0.1:9000/来连接 VM Service。需要注意端口转发规则在设备断开连接后通常会失效自动化脚本里要加入重新建立转发的逻辑。转发建立之后先手动 curl 一下地址确认能返回 JSON再进入下一步。这一步的验证非常重要很多人就是跳过了它导致后面所有脚本报错时都不知道问题出在端口还是数据格式。3.4 连接成功之后先手动导出覆盖率 JSON在写任何自动化脚本之前我建议先手动验证一遍数据链路。第一步拿到 VM 信息curl http://127.0.0.1:9000/getVM第二步拿到 isolate 列表curl http://127.0.0.1:9000/getVM | jq .isolates第三步对每个 isolate 请求源码覆盖报告curl http://127.0.0.1:9000/getSourceReport?isolateIdidscriptIdscriptIdcoveragetrue这一步返回的是 Dart VM 原始的 CodeCoverage 格式包含 ScriptRef 和 ranges 数组。把这个 JSON 保存下来作为后续路径转换的输入。如果这一步能拿到真实数据说明链路已经通了百分之八十。我自己做适配时会先把这份原始 JSON 妥善存好后面无论是调格式转换还是排查路径问题都有据可查。4. 第二道坎拿到 Coverage JSON 之后路径全是“异世界地址”4.1 设备路径和仓库路径为什么对不上Dart VM 返回的 coverage JSON 里每个脚本都会带一个 uri。标准开发机上是file:///repo/lib/foo.dart这样的格式而在鸿蒙设备上脚本地址通常是安装包运行目录下的产物形如file:///data/app/随机目录/lib/foo.dart或者是/data/storage/el2/base/haps/...这类沙箱路径。genhtml 渲染 lcov 报告时会根据 SF 字段去读源码文件。如果 SF 字段是设备路径在开发机上自然找不到文件报告里的代码就会全部消失覆盖率数据也因为源码缺失而无法对应到具体行。这个问题看起来只是路径字符串的小事实际影响很大。我见过有人把这份设备路径的 lcov 报告直接提交到 CI最终覆盖率摘要看起来是有的但网页点进去代码区域全是空白行等于报告白出了。4.2 用脚本把设备路径归位到仓库相对路径解决办法是做一个“路径重映射”。思路很简单把 SF 字段里的设备前缀替换成仓库目录再转成相对路径。具体可以用一个 Python 或 Dart 脚本来实现。这里有一个完整的 Python 示例输入原始 lcov.info输出归一化后的 lcov.infoimport re import sys def remap(raw_lcov, repo_root, out): with open(raw_lcov, r, encodingutf-8) as f: lines f.readlines() with open(out, w, encodingutf-8) as f: for line in lines: if line.startswith(SF:file:///data/): # 把设备路径里的相对源码段提取出来 # 通常是 /lib/... 或 /src/... 或 /test/... match re.search(r/(lib|src|test|core)/.\.dart$, line) if match: rel match.group(0).lstrip(/) f.write(SF: repo_root / rel \n) continue f.write(line) if __name__ __main__: remap(sys.argv[1], sys.argv[2], sys.argv[3])注意这个脚本里的正则要根据实际情况调整。鸿蒙设备上的路径前缀不一定都是/data/也可能带haps、el2等子目录。最稳的做法是先把原始 lcov.info 里的 SF 行逐条打印出来人工确认前缀格式再写死对应的匹配规则。路径映射没有“万能正解”因为鸿蒙各个版本的沙箱路径不完全一致但这步做一次就够了。4.3 格式转换其实不需要完全自己写test_cov 背后用的 coverage 包自带转换能力命令行调用方式如下dart run coverage:format_coverage \ --lcov \ --incoverage.json \ --outlcov_raw.info \ --report-onlib/这条命令能直接把手动从 VM Service 拿到的原始 JSON 转成 lcov 格式。但如果需要路径归一化还是得在前后加一步替换。我目前推荐的组合是这样手动或脚本采集 VM Service JSON用 coverage 包转成 lcov_raw.info用路径重映射脚本生成最终的 lcov.info再用 genhtml 生成 HTML 报告。这套组合可以拆成两个独立的阶段排查问题也方便采集阶段只关心 JSON 有没有数据转换阶段只关心格式对不对。如果最后报告有问题先确认是采集阶段的问题还是转换阶段的问题不要混在一起改否则很容易把环境问题当成代码问题。5. 报告生成与阈值检查LCov 在鸿蒙工程里的实际落地5.1 从 lcov.info 到 HTML 报告拿到归一化的 lcov.info 之后报告生成就回归标准流程了。在开发机上安装 lcov 工具然后执行genhtml -o coverage_report lcov.info打开coverage_report/index.html就能看到所有源文件的覆盖率摘要点击具体文件还能看到每一行的命中情况非常适合用来判断哪些代码是真没测到哪些只是路径没映射上。在 CI 环境里通常还会加一句lcov --summary lcov.info输出里的lines......: 86.4% (130 of 150)就是整个工程的语句覆盖率。这一步的结果可以进一步接到质量门禁上覆盖率低于目标值流水线直接失败。5.2 阈值设定应该怎么选很多团队会把 test_cov 的--min-coverage参数直接写死在命令里比如要求 80%。但这里有个坑数值本身会因为统计口径不同而波动。有的工具统计行覆盖率有的统计分支覆盖率还有的会把测试文件本身也纳入统计。我的建议是初始阶段先把阈值压到 60% 左右先跑通流程然后认真看报告里哪些是必须覆盖的核心代码哪些是自动生成的 build 文件接着用--report-onlib/只统计 lib 目录把生成代码排除掉之后再按模块逐步提高阈值。一次定太高团队会产生抵触情绪反而推行不下去。5.3 多模块工程如何合并覆盖率鸿蒙工程通常有多个模块单元测试也可能分散在多个目录。lcov 支持把多个 tracefile 合并lcov --add-tracefile lcov_hap1.info --add-tracefile lcov_hap2.info --output-file lcov_all.info然后再对合并后的文件统一生成报告。这个方法在 CI 里很常用但要注意每次跑测试前清理旧的 lcov 文件避免把历史数据混进去把覆盖率“跑虚高”。我之前就因为没清干净合并后的覆盖率比真实值高了十多个点查了很久才发现是上一次跑完的报告残留导致的。6. 后面这仨坑每一个都能让人排查一下午6.1 端口号拿到了开发机却一直 curl 超时现象很典型日志里明明打印了 VM Service 地址但开发机访问超时。我的排查链路是确认地址是127.0.0.1还是设备 IP。如果 VM Service 只监听设备回环地址开发机无法直连确认 hdc 端口转发是否建立。实机插拔后转发可能失效重新执行 fport 命令先用 hdc 进入设备在设备内部用 curl 访问确认服务本身是活的检查防火墙或代理有些公司网络环境会拦截随机端口。大多数情况都出在第二步拿着旧的端口映射去连新设备或者压根没建映射。把这个检查做成脚本的固定开场白能省掉大量无用功。6.2 覆盖率要么 100%要么 0%中间值永远出不来这个现象非常经典。如果报告里每行覆盖率都是 100%多半是测试进程根本没跑起来VM 没有执行任何被测代码或者采集时选错了 isolate选中了空闲状态的 isolate。反过来如果全是 0%可能是测试代码路径没有命中被测源码比如被测模块因为平台能力缺失被 mock 掉了。排查时要回看 coverage.json 里的 ranges如果所有 range 的 executed 字段都是 false先检查测试进程是否真的执行完如果 ranges 为空大概率是源文件路径匹配不上转换时把全部 DA 行映射到了同一行。这个坑的排查要点在于不要只看最终报告一定要保留中间 JSON 产物。6.3 中文路径和转义符号导致的报告渲染翻车鸿蒙工程无论是项目名还是源文件路径都可能有中文。lcov 对 unicode 路径的支持整体还行但跨平台传递时容易出现编码不一致。典型表现是终端里看 lcov.info 是正常的genhtml 却报文件找不到。解决办法是统一编码。建议所有脚本读写 lcov.info 时强制使用 utf-8 编码并在 genhtml 前把 SF 行做一次简单验证用脚本检查每个 SF 对应的文件是否真实存在。如果文件不存在说明路径重映射没覆盖到某些特殊情况直接输出去排查映射规则。文件名的空格、#、?等特殊字符也会在 URL 转换时被转义写正则替换时最好匹配整个文件 URI而不是只匹配路径前缀。写到这里我自己再回头看这次适配过程最大的感受是鸿蒙化适配 test_cov真正难的地方并不是要改某个库的代码而是被逼着把 Dart VM 覆盖率采集的完整机制重新学了一遍。你只有完全理解 VM Service、CodeCoverage JSON 和 lcov 格式之间的转换关系才能在设备差异、路径差异、依赖差异这三座大山面前不慌。如果你最近也在做类似的适配建议按这个思路走先最小工程跑通再手动验证 VM Service再上路径映射脚本最后接 CI。每一步的产出都是可见的排查起来会舒服很多。适配完了再回头看这套链路其实已经比原来在标准 Flutter 上的理解更深了一层也算是个意外收获。
返回列表