文章目录前言鸿蒙测试框架概览单元测试基本函数测试异步测试Mock 能力UI 测试性能测试测试配置与执行一些实用建议前言说实话大部分鸿蒙开发者对测试这块不太重视。原因也简单——赶进度的时候谁有空写测试但等到线上出了 Bug 被用户骂的时候才后悔当初没多写几个用例。鸿蒙的测试框架ohos.hypium现在已经比较成熟了覆盖了单元测试、UI 自动化测试和性能测试。我最近给团队的新闻列表模块补了一套完整的测试跑通之后感觉确实靠谱今天分享一下整个链路。鸿蒙测试框架概览ohos.hypium是 HarmonyOS 的官方测试框架提供三种测试能力测试类型用途执行位置单元测试函数、类、业务逻辑DevEco Studio 内UI 测试组件渲染、交互、截图对比模拟器/真机性能测试FPS、内存、启动耗时真机推荐项目里测试代码放在ohosTest目录下跟src/main同级entry/ ├── src/ │ ├── main/ # 业务代码 │ │ └── ets/ │ └── ohosTest/ # 测试代码 │ └── ets/ │ ├── test/ │ │ ├── List.test.ets │ │ └── UiTest.test.ets │ └── testability/ │ └── pages/单元测试单元测试是最基础的用来验证函数逻辑、数据处理、业务规则。基本函数测试import{describe,it,expect}fromohos/hypiumimport{NewsUtils}from../main/ets/utils/NewsUtilsexportdefaultfunctionunitTests(){describe(NewsUtils 测试,(){it(标题截断超长标题应该截断并加省略号,0,(){constlongTitle这是一条非常非常长的新闻标题用来测试截断逻辑是否正确工作constresultNewsUtils.truncateTitle(longTitle,20)expect(result.length).assertLessOrEqual(23)// 20 ...expect(result.endsWith(...)).assertTrue()})it(标题截断短标题不应该被截断,0,(){constshortTitle今日要闻constresultNewsUtils.truncateTitle(shortTitle,20)expect(result).assertEqual(今日要闻)})it(时间格式化今天应该显示时分,0,(){consttodaynewDate()today.setHours(14,30,0)constresultNewsUtils.formatTime(today)expect(result).assertEqual(14:30)})it(时间格式化昨天应该显示昨天,0,(){constyesterdaynewDate()yesterday.setDate(yesterday.getDate()-1)constresultNewsUtils.formatTime(yesterday)expect(result).assertEqual(昨天)})})}异步测试网络请求、数据库操作这些异步逻辑用async/await配合expect来测试import{describe,it,expect,beforeAll,afterAll}fromohos/hypiumimport{NewsRepository}from../main/ets/repository/NewsRepositoryexportdefaultfunctionasyncTests(){describe(NewsRepository 异步测试,(){it(获取新闻列表应返回非空数组,0,async(){constreponewNewsRepository()constnewsListawaitrepo.fetchNewsList(1,20)expect(newsList).assertNotNull()expect(newsList.length).assertLarger(0)expect(newsList[0].title).assertNotNull()})it(分页参数正确传递,0,async(){constreponewNewsRepository()constpage1awaitrepo.fetchNewsList(1,10)constpage2awaitrepo.fetchNewsList(2,10)// 两页数据不应该完全相同expect(page1[0].id).assertNotEqual(page2[0].id)})it(非法页码应返回空数组,0,async(){constreponewNewsRepository()constresultawaitrepo.fetchNewsList(-1,10)expect(result.length).assertEqual(0)})})}Mock 能力依赖外部服务的逻辑用 Mock 隔离掉import{describe,it,expect}fromohos/hypiumimport{NewsViewModel}from../main/ets/viewmodel/NewsViewModelexportdefaultfunctionmockTests(){describe(NewsViewModel Mock 测试,(){it(刷新后 shouldUpdate 标记应该被清除,0,(){constvmnewNewsViewModel()// Mock 数据源constmockData[{id:1,title:新闻1,category:tech,timestamp:Date.now()},{id:2,title:新闻2,category:sports,timestamp:Date.now()},]vm.setMockData(mockData)expect(vm.shouldUpdate).assertTrue()vm.refreshData()expect(vm.shouldUpdate).assertFalse()expect(vm.displayList.length).assertEqual(2)})it(按分类筛选,0,(){constvmnewNewsViewModel()vm.setMockData([{id:1,title:AI 突破,category:tech,timestamp:Date.now()},{id:2,title:足球赛果,category:sports,timestamp:Date.now()},{id:3,title:5G 进展,category:tech,timestamp:Date.now()},])vm.filterByCategory(tech)expect(vm.displayList.length).assertEqual(2)expect(vm.displayList[0].title).assertEqual(AI 突破)})})}UI 测试UI 测试用来验证组件渲染是否正确、用户交互是否符合预期。鸿蒙的 UI 测试通过UiDriver和UiComponent来操作。import{describe,it,expect,beforeAll,afterAll}fromohos/hypiumimport{UiDriver,UiComponent,DriverExtensionApi}fromkit.TestKitexportdefaultfunctionuiTests(){letdriver:UiDriverbeforeAll((){drivernewUiDriver()})afterAll((){driver.destroy()})describe(新闻列表页 UI 测试,(){it(页面加载后应显示新闻列表,0,async(){// 等待页面渲染完成awaitdriver.delayMs(2000)// 通过文本查找组件constlistTitleawaitdriver.findComponentByText(今日新闻)expect(listTitle).assertNotNull()// 验证列表项存在constfirstItemawaitdriver.findComponentByText(新闻标题1)expect(firstItem).assertNotNull()})it(下拉刷新应该更新列表,0,async(){// 获取列表组件constlistawaitdriver.findComponentByType(List)expect(list).assertNotNull()// 模拟下拉刷新从顶部往下拖awaitdriver.swipe(500,300,500,800,20)awaitdriver.delayMs(1500)// 刷新后检查刷新提示constrefreshTextawaitdriver.findComponentByText(已刷新)// 刷新提示可能一闪而过这里检查列表数据已更新即可constupdatedItemawaitdriver.findComponentByText(新闻标题1)expect(updatedItem).assertNotNull()})it(点击新闻项应跳转到详情页,0,async(){constnewsItemawaitdriver.findComponentByText(新闻标题1)awaitnewsItem.click()awaitdriver.delayMs(1000)// 验证详情页的返回按钮存在constbackBtnawaitdriver.findComponentByText(返回)expect(backBtn).assertNotNull()// 返回上一页awaitbackBtn.click()awaitdriver.delayMs(500)})it(截图对比测试,0,async(){awaitdriver.delayMs(1000)// 截取当前屏幕constscreenshotawaitdriver.screenCap()expect(screenshot).assertNotNull()// 保存截图用于后续对比// 首次运行生成基准图后续运行与基准图对比constcompareResultawaitdriver.compareScreen(screenshot,{threshold:0.01,// 允许 1% 的像素差异baselinePath:src/ohosTest/resources/baseline/news_list.png})expect(compareResult.isMatch).assertTrue()})})}性能测试性能测试在真机上跑比较准模拟器数据不太靠谱。import{describe,it,expect,beforeAll}fromohos/hypiumimport{performanceTest,PerfMetrics}fromkit.TestKitexportdefaultfunctionperfTests(){describe(新闻列表页性能测试,(){it(列表滑动帧率应大于 55fps,0,async(){constdrivernewUiDriver()// 开始录制帧率awaitperformanceTest.startFrameMonitor()// 模拟快速滑动列表constlistawaitdriver.findComponentByType(List)for(leti0;i5;i){awaitdriver.swipe(500,800,500,200,10)awaitdriver.delayMs(300)}// 停止录制获取帧率数据constmetrics:PerfMetricsawaitperformanceTest.stopFrameMonitor()expect(metrics.averageFps).assertLarger(55)expect(metrics.jankCount).assertLess(5)// 卡顿次数少于 5 次expect(metrics.maxFrameTime).assertLess(33)// 最大帧耗时小于 33ms约30fpsdriver.destroy()})it(页面启动耗时应该小于 500ms,0,async(){// 启动耗时监控conststartTimeperformanceTest.getTimestamp()// 模拟页面导航constdrivernewUiDriver()awaitdriver.navigateTo(pages/NewsList)awaitdriver.waitUntilComponentVisible(news_list_container,3000)constendTimeperformanceTest.getTimestamp()constloadTimeendTime-startTimeexpect(loadTime).assertLess(500)// 启动耗时 500msdriver.destroy()})it(100条数据加载后内存增量应小于 30MB,0,async(){constmemBeforeperformanceTest.getMemoryUsage()// 加载 100 条数据constdrivernewUiDriver()constloadMoreawaitdriver.findComponentByText(加载更多)for(leti0;i5;i){awaitloadMore.click()awaitdriver.delayMs(500)}constmemAfterperformanceTest.getMemoryUsage()constmemDeltamemAfter.totalPss-memBefore.totalPss// 内存增量不超过 30MBexpect(memDelta).assertLess(30*1024)// 单位 KBdriver.destroy()})})}测试配置与执行在 DevEco Studio 里运行测试需要在build-profile.json5里确保 ohosTest 模块被包含{ modules: [ { name: entry, srcPath: ./entry, targets: [ { name: default, applyToProducts: [default] } ] } ] }运行方式IDE 内运行右键测试文件 → Run ‘ohosTest’命令行运行hvigorw assembleHap --no-daemon然后通过 hdc 安装到设备上执行一些实用建议单元测试的覆盖率目标业务逻辑层ViewModel、Repository争取 80% 以上UI 层 60% 就够了。别追求 100%那会花太多时间在边界条件上。UI 测试不稳定怎么办UI 测试受设备性能影响很大delayMs的等待时间不要太抠宁可多等一会儿。用waitForComponent代替硬编码的等待时间更靠谱。性能测试的基准线第一次跑的时候把数据记录下来作为 baseline后续 CI 每次跑完跟 baseline 对比偏差超过 10% 就告警。别在 CI 上跑真机性能测试CI 的模拟器性能跟真机差太多性能数据没有参考意义。建议搞一台专门的测试机定期跑性能回归。写测试确实花时间但那种改了一行代码跑一遍测试全绿的安心感是任何东西都换不来的。我的建议是从单元测试开始先把业务逻辑的测试补上UI 测试慢慢来。别一上来就想搞全链路覆盖那样大概率坚持不下去。