HarmonyOS7 测试别只停在单测: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 } from '@ohos/hypium'
import { NewsUtils } from '../main/ets/utils/NewsUtils'
export default function unitTests() {
describe('NewsUtils 测试', () => {
it('标题截断:超长标题应该截断并加省略号', 0, () => {
const longTitle = '这是一条非常非常长的新闻标题用来测试截断逻辑是否正确工作'
const result = NewsUtils.truncateTitle(longTitle, 20)
expect(result.length).assertLessOrEqual(23) // 20 + '...'
expect(result.endsWith('...')).assertTrue()
})
it('标题截断:短标题不应该被截断', 0, () => {
const shortTitle = '今日要闻'
const result = NewsUtils.truncateTitle(shortTitle, 20)
expect(result).assertEqual('今日要闻')
})
it('时间格式化:今天应该显示时分', 0, () => {
const today = new Date()
today.setHours(14, 30, 0)
const result = NewsUtils.formatTime(today)
expect(result).assertEqual('14:30')
})
it('时间格式化:昨天应该显示"昨天"', 0, () => {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
const result = NewsUtils.formatTime(yesterday)
expect(result).assertEqual('昨天')
})
})
}
异步测试

网络请求、数据库操作这些异步逻辑,用 async/await 配合 expect 来测试:
import { describe, it, expect, beforeAll, afterAll } from '@ohos/hypium'
import { NewsRepository } from '../main/ets/repository/NewsRepository'
export default function asyncTests() {
describe('NewsRepository 异步测试', () => {
it('获取新闻列表应返回非空数组', 0, async () => {
const repo = new NewsRepository()
const newsList = await repo.fetchNewsList(1, 20)
expect(newsList).assertNotNull()
expect(newsList.length).assertLarger(0)
expect(newsList[0].title).assertNotNull()
})
it('分页参数正确传递', 0, async () => {
const repo = new NewsRepository()
const page1 = await repo.fetchNewsList(1, 10)
const page2 = await repo.fetchNewsList(2, 10)
// 两页数据不应该完全相同
expect(page1[0].id).assertNotEqual(page2[0].id)
})
it('非法页码应返回空数组', 0, async () => {
const repo = new NewsRepository()
const result = await repo.fetchNewsList(-1, 10)
expect(result.length).assertEqual(0)
})
})
}
Mock 能力
依赖外部服务的逻辑,用 Mock 隔离掉:

import { describe, it, expect } from '@ohos/hypium'
import { NewsViewModel } from '../main/ets/viewmodel/NewsViewModel'
export default function mockTests() {
describe('NewsViewModel Mock 测试', () => {
it('刷新后 shouldUpdate 标记应该被清除', 0, () => {
const vm = new NewsViewModel()
// Mock 数据源
const mockData = [
{ 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, () => {
const vm = new NewsViewModel()
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 } from '@ohos/hypium'
import { UiDriver, UiComponent, DriverExtensionApi } from '@kit.TestKit'
export default function uiTests() {
let driver: UiDriver
beforeAll(() => {
driver = new UiDriver()
})
afterAll(() => {
driver.destroy()
})
describe('新闻列表页 UI 测试', () => {
it('页面加载后应显示新闻列表', 0, async () => {
// 等待页面渲染完成
await driver.delayMs(2000)
// 通过文本查找组件
const listTitle = await driver.findComponentByText('今日新闻')
expect(listTitle).assertNotNull()
// 验证列表项存在
const firstItem = await driver.findComponentByText('新闻标题1')
expect(firstItem).assertNotNull()
})
it('下拉刷新应该更新列表', 0, async () => {
// 获取列表组件
const list = await driver.findComponentByType('List')
expect(list).assertNotNull()
// 模拟下拉刷新:从顶部往下拖
await driver.swipe(500, 300, 500, 800, 20)
await driver.delayMs(1500)
// 刷新后检查刷新提示
const refreshText = await driver.findComponentByText('已刷新')
// 刷新提示可能一闪而过,这里检查列表数据已更新即可
const updatedItem = await driver.findComponentByText('新闻标题1')
expect(updatedItem).assertNotNull()
})
it('点击新闻项应跳转到详情页', 0, async () => {
const newsItem = await driver.findComponentByText('新闻标题1')
await newsItem.click()
await driver.delayMs(1000)
// 验证详情页的返回按钮存在
const backBtn = await driver.findComponentByText('返回')
expect(backBtn).assertNotNull()
// 返回上一页
await backBtn.click()
await driver.delayMs(500)
})
it('截图对比测试', 0, async () => {
await driver.delayMs(1000)
// 截取当前屏幕
const screenshot = await driver.screenCap()
expect(screenshot).assertNotNull()
// 保存截图用于后续对比
// 首次运行生成基准图,后续运行与基准图对比
const compareResult = await driver.compareScreen(screenshot, {
threshold: 0.01, // 允许 1% 的像素差异
baselinePath: 'src/ohosTest/resources/baseline/news_list.png'
})
expect(compareResult.isMatch).assertTrue()
})
})
}
性能测试
性能测试在真机上跑比较准,模拟器数据不太靠谱。
import { describe, it, expect, beforeAll } from '@ohos/hypium'
import { performanceTest, PerfMetrics } from '@kit.TestKit'
export default function perfTests() {
describe('新闻列表页性能测试', () => {
it('列表滑动帧率应大于 55fps', 0, async () => {
const driver = new UiDriver()
// 开始录制帧率
await performanceTest.startFrameMonitor()
// 模拟快速滑动列表
const list = await driver.findComponentByType('List')
for (let i = 0; i < 5; i++) {
await driver.swipe(500, 800, 500, 200, 10)
await driver.delayMs(300)
}
// 停止录制,获取帧率数据
const metrics: PerfMetrics = await performanceTest.stopFrameMonitor()
expect(metrics.averageFps).assertLarger(55)
expect(metrics.jankCount).assertLess(5) // 卡顿次数少于 5 次
expect(metrics.maxFrameTime).assertLess(33) // 最大帧耗时小于 33ms(约30fps)
driver.destroy()
})
it('页面启动耗时应该小于 500ms', 0, async () => {
// 启动耗时监控
const startTime = performanceTest.getTimestamp()
// 模拟页面导航
const driver = new UiDriver()
await driver.navigateTo('pages/NewsList')
await driver.waitUntilComponentVisible('news_list_container', 3000)
const endTime = performanceTest.getTimestamp()
const loadTime = endTime - startTime
expect(loadTime).assertLess(500) // 启动耗时 < 500ms
driver.destroy()
})
it('100条数据加载后内存增量应小于 30MB', 0, async () => {
const memBefore = performanceTest.getMemoryUsage()
// 加载 100 条数据
const driver = new UiDriver()
const loadMore = await driver.findComponentByText('加载更多')
for (let i = 0; i < 5; i++) {
await loadMore.click()
await driver.delayMs(500)
}
const memAfter = performanceTest.getMemoryUsage()
const memDelta = memAfter.totalPss - memBefore.totalPss
// 内存增量不超过 30MB
expect(memDelta).assertLess(30 * 1024) // 单位 KB
driver.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 测试慢慢来。别一上来就想搞全链路覆盖,那样大概率坚持不下去。
更多推荐




所有评论(0)