HarmonyOS7 组件库质量怎么兜底?单测、快照和视觉回归测试一次补齐

📅 2026/7/1 18:21:05
HarmonyOS7 组件库质量怎么兜底?单测、快照和视觉回归测试一次补齐
文章目录前言组件库测试的三个层次单元测试隔离验证组件逻辑Mock 依赖让测试跑得起来UI 测试验证组件渲染快照测试UI 变化的自动检测测试覆盖率与 CI 集成踩坑总结小结前言做组件库最怕什么不是写不出来是改着改着就坏了。上周改了个 Button 组件的间距结果把整个表单页面的布局顶歪了。改了个 Input 的样式结果暗色模式下字都看不见。组件被几十个页面引用出了问题排查起来要命。这就是为什么组件库需要专门的测试策略——它跟业务代码的测试不一样关注的重点完全不同。组件库测试的三个层次组件库测试我分成三层从里到外单元测试验证组件的逻辑正确性比如状态切换、事件触发、边界条件UI 测试验证组件渲染出来是对的结构、样式、交互都没问题快照测试验证组件的 UI 没有意外变化每次改动都有记录三层组合起来才算给组件上了保险。单元测试隔离验证组件逻辑单元测试的核心原则是隔离——不依赖真实的网络、数据库、文件系统。组件库里的工具函数和逻辑类是最适合单测的部分。来看一个典型的组件工具类测试// __tests__/utils/validator.test.etsimport{describe,it,expect}fromohos/hypiumimport{FormValidator}from../../src/utils/FormValidatorexportfunctionvalidatorTest(){describe(FormValidator,(){it(邮箱格式校验-合法输入,(){constresultFormValidator.isEmail(testexample.com)expect(result).assertTrue()})it(邮箱格式校验-缺少符号,(){constresultFormValidator.isEmail(testexample.com)expect(result).assertFalse()})it(手机号校验-11位合法号码,(){constresultFormValidator.isPhone(13800138000)expect(result).assertTrue()})it(手机号校验-位数不足,(){constresultFormValidator.isPhone(1380013)expect(result).assertFalse()})it(必填字段校验-空字符串返回错误信息,(){constresultFormValidator.required(,用户名)expect(result).assertEqual(用户名不能为空)})it(必填字段校验-有值返回空,(){constresultFormValidator.required(张三,用户名)expect(result).assertEqual()})})}每个it只测一个场景测试名用中文描述清楚什么输入→什么结果。这样测试报告一眼就能看出哪个用例挂了。Mock 依赖让测试跑得起来组件经常会依赖外部服务比如网络请求、存储、事件总线。测试的时候必须把这些依赖 Mock 掉// __tests__/components/SmartSearch.test.etsimport{describe,it,expect,beforeEach}fromohos/hypiumimport{SmartSearchViewModel}from../../src/viewmodels/SmartSearchViewModel// Mock 搜索服务classMockSearchService{privatemockResults:SearchResult[][]privateshouldFail:booleanfalsesetMockResults(results:SearchResult[]):void{this.mockResultsresults}setShouldFail(fail:boolean):void{this.shouldFailfail}asyncsearch(keyword:string):PromiseSearchResult[]{if(this.shouldFail){thrownewError(网络请求失败)}returnthis.mockResults.filter(rr.title.includes(keyword))}}exportfunctionsmartSearchTest(){describe(SmartSearchViewModel,(){letviewModel:SmartSearchViewModelletmockService:MockSearchServicebeforeEach((){mockServicenewMockSearchService()viewModelnewSmartSearchViewModel(mockService)})it(搜索关键词-返回过滤结果,async(){mockService.setMockResults([{id:1,title:鸿蒙开发入门},{id:2,title:鸿蒙架构设计},{id:3,title:Flutter 入门指南}])constresultsawaitviewModel.search(鸿蒙)expect(results.length).assertEqual(2)expect(results[0].title).assertEqual(鸿蒙开发入门)})it(搜索空关键词-不发起请求,async(){constresultsawaitviewModel.search()expect(results.length).assertEqual(0)})it(网络失败-返回空数组并设置错误状态,async(){mockService.setShouldFail(true)constresultsawaitviewModel.search(鸿蒙)expect(results.length).assertEqual(0)expect(viewModel.errorState).assertEqual(网络请求失败)})it(防抖-快速连续输入只发最后一次请求,async(){mockService.setMockResults([{id:1,title:结果}])// 快速连续调用viewModel.search(鸿)viewModel.search(鸿蒙)viewModel.search(鸿蒙开)// 等待防抖结束awaitnewPromise(resolvesetTimeout(resolve,350))expect(viewModel.searchCount).assertEqual(1)})})}注意这里beforeEach每次测试前都重新创建 ViewModel 和 Mock保证测试之间互不干扰。防抖那个测试用setTimeout等待350ms 大于防抖间隔通常 300ms。UI 测试验证组件渲染UI 测试关注的是组件实际渲染出来的东西对不对。HarmonyOS 提供了 UI 测试框架可以操作组件并断言 UI 状态// __tests__/ui/Button.test.etsimport{describe,it,expect}fromohos/hypiumimport{UiDriver,Component}fromohos.UiTestFrameworkexportfunctionbuttonUITest(){describe(Button 组件 UI 测试,(){it(默认状态-显示正确文本和样式,async(){constdrivernewUiDriver()awaitdriver.loadComponent(AppButton)consttextNodeawaitdriver.findByText(确认)expect(textNode.exists()).assertTrue()expect(textNode.getFontSize()).assertEqual(16)constcontainerawaitdriver.findById(btn_container)expect(container.getBackgroundColor()).assertEqual(#007DFF)expect(container.getBorderRadius()).assertEqual(8)})it(disabled 状态-样式变灰且不可点击,async(){constdrivernewUiDriver()awaitdriver.loadComponent(AppButton,{disabled:true})constcontainerawaitdriver.findById(btn_container)expect(container.getBackgroundColor()).assertEqual(#C0C0C0)expect(container.isEnabled()).assertFalse()})it(loading 状态-显示加载动画,async(){constdrivernewUiDriver()awaitdriver.loadComponent(AppButton,{loading:true})constloadingIconawaitdriver.findById(loading_icon)expect(loadingIcon.exists()).assertTrue()// 原文本应该隐藏consttextNodeawaitdriver.findByText(确认)expect(textNode.isVisible()).assertFalse()})it(点击事件-触发 onClick 回调,async(){letclickedfalseconstdrivernewUiDriver()awaitdriver.loadComponent(AppButton,{onClick:(){clickedtrue}})constbtnawaitdriver.findById(btn_container)awaitbtn.click()expect(clicked).assertTrue()})})}快照测试UI 变化的自动检测快照测试是组件库的杀手锏。原理很简单第一次跑测试时把组件渲染结果保存为快照文件。之后每次跑测试把当前渲染结果和快照对比有差异就报错。// __tests__/snapshot/Card.snapshot.etsimport{describe,it,expect}fromohos/hypiumimport{SnapshotTester}from../helpers/SnapshotTesterexportfunctioncardSnapshotTest(){describe(Card 组件快照测试,(){it(默认卡片-快照对比,async(){consttesternewSnapshotTester()constsnapshotawaittester.render(AppCard,{title:测试卡片,description:这是一段描述文本,imageUrl:https://example.com/img.png})// 首次运行生成快照后续运行对比expect(snapshot).toMatchSnapshot(card_default)})it(带操作按钮的卡片-快照对比,async(){consttesternewSnapshotTester()constsnapshotawaittester.render(AppCard,{title:可操作卡片,showActions:true,actions:[编辑,删除]})expect(snapshot).toMatchSnapshot(card_with_actions)})it(暗色模式-快照对比,async(){consttesternewSnapshotTester()tester.setTheme(dark)constsnapshotawaittester.render(AppCard,{title:暗色卡片,description:Dark mode test})expect(snapshot).toMatchSnapshot(card_dark_mode)})})}SnapshotTester的实现思路// __tests__/helpers/SnapshotTester.etsimport{screenshot}fromohos.UiTestFrameworkexportclassSnapshotTester{privatetheme:stringlightprivatesnapshotDir:string__snapshots__setTheme(theme:string):void{this.themetheme}asyncrender(componentName:string,props:Recordstring,any):PromiseSnapshotData{// 渲染组件并截图constpixelDataawaitscreenshot(componentName,props,this.theme)return{componentName,theme:this.theme,props,pixelData,timestamp:Date.now()}}}interfaceSnapshotData{componentName:stringtheme:stringprops:Recordstring,anypixelData:Uint8Array timestamp:number}快照文件存成图片跑测试时用像素级对比。有差异就生成一个 diff 图片高亮标出变化区域一眼就能看出哪里变了。测试覆盖率与 CI 集成光写测试不够还得保证覆盖率。在 hvigor 配置里加上测试覆盖率要求{testOptions:{coverage:{enabled:true,threshold:{lines:80,branches:70,functions:85},exclude:[**/__tests__/**,**/index.ets]}}}CI 流水线里跑测试时覆盖率不达标直接构建失败。这样倒逼团队保持测试质量。CI 配置大致长这样# .ci/test-pipeline.ymlstages:-lint-unit-test-ui-test-snapshot-test-coverage-checkunit-test:stage:unit-testscript:-hvigorw assembleHar--no-daemon-hvigorw runTest--modulelib--typeunitartifacts:reports:junit:build/test-results/unit/*.xmlsnapshot-test:stage:snapshot-testscript:-hvigorw runTest--modulelib--typesnapshotartifacts:paths:-build/test-results/snapshot-diff/when:on_failurecoverage-check:stage:coverage-checkscript:-hvigorw checkCoverage--threshold80allow_failure:false踩坑总结快照测试不要无脑更新。跑测试报快照不匹配时先看清楚 diff 再决定是更新快照还是修 bug。无脑--updateSnapshot会让快照测试形同虚设。Mock 要尽量贴近真实。Mock 得太假会导致测试过了、上线挂了。比如网络请求的 Mock 最好模拟超时、断网、500 错误这些异常场景。异步测试别忘了 await。ArkTS 里大量操作是异步的测试里不 await 的话断言可能在异步操作完成前就执行了导致假通过。小结组件库测试的核心目标就一个让改组件的人有信心。单元测试保证逻辑不坏UI 测试保证渲染正确快照测试保证变化可控。三层叠起来改代码的时候心里就不慌了。我建议在组件库里每个组件至少配套一个单测文件和一个快照测试文件。初期写测试确实花时间但长期来看省下的回归测试时间和线上 bug 修复时间远远超过投入。特别是组件库被多个项目引用的时候一个 bug 影响面是乘数级的测试真的不能省。