[鸿蒙从零到一] HarmonyOS 单元测试与 UI 自动化测试实战:从代码质量到用户体验的全链路保障

引言

测试是软件工程中不可或缺的环节。在 HarmonyOS 应用开发中,良好的测试策略能够有效提升代码质量、减少线上故障、加速迭代节奏。本文将系统性地介绍 HarmonyOS 应用的单元测试与 UI 自动化测试实战,涵盖测试框架选型、用例编写、Mock 技巧、UI 交互验证以及持续集成接入,帮助开发者建立完整的测试体系。

一、HarmonyOS 测试体系概览

1.1 测试分层模型

HarmonyOS 应用测试遵循经典的测试金字塔模型:

  • 单元测试(Unit Test):测试单个函数、类或模块的逻辑正确性,执行速度快,占比最高
  • 集成测试(Integration Test):测试模块间协作、数据流转、API 调用等场景
  • UI 自动化测试(UI Test):模拟用户操作,验证界面交互、页面跳转、状态更新等端到端流程

1.2 HarmonyOS 测试框架

HarmonyOS 官方提供了完整的测试框架支持:

  • ArkTS 单元测试框架:基于 Jest 风格的测试 API,支持同步与异步断言
  • UI 测试框架(UiTest):提供组件查找、事件注入、属性断言等能力
  • 测试运行器(Test Runner):集成在 DevEco Studio 中,支持一键运行与调试

二、单元测试实战

2.1 测试环境搭建

在 DevEco Studio 创建的工程中,测试代码默认位于 entry/src/ohosTest/ets/test 目录。测试文件命名遵循 *.test.ets 规范。

典型的测试用例结构:

typescript 复制代码
import { describe, it, expect } from '@ohos/hypium'

export default function abilityTest() {
  describe('UserService', () => {
    it('should return user info when login success', async () => {
      const service = new UserService()
      const result = await service.login('test', 'password')
      expect(result.success).assertTrue()
      expect(result.userName).assertEqual('test')
    })
  })
}

2.2 业务逻辑单元测试

针对业务逻辑层(如数据处理、状态计算、工具函数),编写纯函数风格的单元测试:

typescript 复制代码
// utils/StringUtil.ets
export class StringUtil {
  static truncate(text: string, maxLength: number): string {
    if (text.length <= maxLength) return text
    return text.substring(0, maxLength) + '...'
  }

  static isEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  }
}

// test/StringUtil.test.ets
import { describe, it, expect } from '@ohos/hypium'
import { StringUtil } from '../../../ets/utils/StringUtil'

export default function stringUtilTest() {
  describe('StringUtil', () => {
    it('should truncate long text', () => {
      const result = StringUtil.truncate('Hello World', 5)
      expect(result).assertEqual('Hello...')
    })

    it('should not truncate short text', () => {
      const result = StringUtil.truncate('Hi', 5)
      expect(result).assertEqual('Hi')
    })

    it('should validate email format', () => {
      expect(StringUtil.isEmail('user@example.com')).assertTrue()
      expect(StringUtil.isEmail('invalid-email')).assertFalse()
    })
  })
}

2.3 异步逻辑测试

对于网络请求、数据库操作等异步场景,使用 async/await 配合 done 回调:

typescript 复制代码
import { describe, it, expect } from '@ohos/hypium'
import { http } from '@kit.NetworkKit'

export default function httpTest() {
  describe('NetworkService', () => {
    it('should fetch user list from API', async (done) => {
      const request = http.createHttp()
      try {
        const response = await request.request('https://api.example.com/users')
        const data = JSON.parse(response.result as string)
        expect(data.code).assertEqual(200)
        expect(data.users.length).assertLarger(0)
        done()
      } catch (err) {
        expect().assertFail()
        done()
      }
    })
  })
}

2.4 Mock 与依赖注入

当测试代码依赖外部服务(如网络、数据库)时,通过 Mock 技术隔离外部依赖:

typescript 复制代码
// service/UserRepository.ets
export interface IUserRepository {
  getUserById(id: string): Promise<User>
}

export class UserRepository implements IUserRepository {
  async getUserById(id: string): Promise<User> {
    // 真实实现调用网络或数据库
    return await http.get(`/users/${id}`)
  }
}

// test/UserViewModel.test.ets
class MockUserRepository implements IUserRepository {
  async getUserById(id: string): Promise<User> {
    return { id, name: 'Mock User', email: 'mock@test.com' }
  }
}

export default function viewModelTest() {
  describe('UserViewModel', () => {
    it('should load user info', async () => {
      const mockRepo = new MockUserRepository()
      const viewModel = new UserViewModel(mockRepo)
      await viewModel.loadUser('123')
      expect(viewModel.userName).assertEqual('Mock User')
    })
  })
}

2.5 状态管理测试

对于使用 @State@Observed 的状态驱动逻辑,可以测试状态变更是否触发预期行为:

typescript 复制代码
@Observed
class CounterState {
  count: number = 0

  increment() {
    this.count++
  }

  decrement() {
    this.count--
  }
}

export default function stateTest() {
  describe('CounterState', () => {
    it('should increment count', () => {
      const state = new CounterState()
      state.increment()
      expect(state.count).assertEqual(1)
    })

    it('should decrement count', () => {
      const state = new CounterState()
      state.count = 5
      state.decrement()
      expect(state.count).assertEqual(4)
    })
  })
}

三、UI 自动化测试实战

3.1 UI 测试框架基础

HarmonyOS 的 UI 测试框架(UiTest)提供了组件查找、事件模拟、属性断言等能力。核心 API:

  • Driver.create():创建测试驱动器
  • driver.findComponent(on):查找组件
  • component.click():模拟点击
  • component.inputText(text):输入文本
  • component.getText():获取文本内容

3.2 组件查找与断言

通过 idtexttype 等属性定位组件并验证状态:

typescript 复制代码
import { describe, it, expect } from '@ohos/hypium'
import { Driver, ON } from '@ohos.UiTest'

export default function loginPageTest() {
  describe('LoginPage', () => {
    it('should display login button', async () => {
      const driver = Driver.create()
      const loginBtn = await driver.findComponent(ON.text('登录'))
      expect(await loginBtn.isEnabled()).assertTrue()
    })

    it('should show error when submit empty form', async () => {
      const driver = Driver.create()
      const submitBtn = await driver.findComponent(ON.id('submitButton'))
      await submitBtn.click()
      const errorMsg = await driver.findComponent(ON.text('请输入用户名'))
      expect(await errorMsg.isVisible()).assertTrue()
    })
  })
}

3.3 输入与交互测试

模拟用户输入、点击、滑动等操作:

typescript 复制代码
export default function formTest() {
  describe('RegisterForm', () => {
    it('should submit form with valid data', async () => {
      const driver = Driver.create()
      
      // 输入用户名
      const usernameInput = await driver.findComponent(ON.id('usernameInput'))
      await usernameInput.inputText('testuser')
      
      // 输入密码
      const passwordInput = await driver.findComponent(ON.id('passwordInput'))
      await passwordInput.inputText('password123')
      
      // 点击提交
      const submitBtn = await driver.findComponent(ON.id('submitButton'))
      await submitBtn.click()
      
      // 验证成功提示
      await driver.delayMs(1000) // 等待网络请求
      const successMsg = await driver.findComponent(ON.text('注册成功'))
      expect(await successMsg.isVisible()).assertTrue()
    })
  })
}

3.4 列表与滚动测试

对于 List、Grid 等可滚动组件,测试滚动行为与数据加载:

typescript 复制代码
export default function listTest() {
  describe('UserList', () => {
    it('should load more data when scroll to bottom', async () => {
      const driver = Driver.create()
      const list = await driver.findComponent(ON.type('List'))
      
      // 获取初始条目数
      const initialItems = await driver.findComponents(ON.type('ListItem'))
      const initialCount = initialItems.length
      
      // 滚动到底部
      await list.scrollToBottom()
      await driver.delayMs(1000)
      
      // 验证条目增加
      const updatedItems = await driver.findComponents(ON.type('ListItem'))
      expect(updatedItems.length).assertLarger(initialCount)
    })
  })
}

3.5 页面跳转测试

验证页面导航与参数传递:

typescript 复制代码
export default function navigationTest() {
  describe('PageNavigation', () => {
    it('should navigate to detail page with correct params', async () => {
      const driver = Driver.create()
      
      // 点击列表第一项
      const firstItem = await driver.findComponent(ON.id('listItem_0'))
      await firstItem.click()
      
      // 验证跳转到详情页
      await driver.delayMs(500)
      const detailTitle = await driver.findComponent(ON.id('detailTitle'))
      const titleText = await detailTitle.getText()
      expect(titleText).assertContain('详情')
    })
  })
}

3.6 弹窗与确认框测试

测试 Dialog、AlertDialog 等弹窗组件:

typescript 复制代码
export default function dialogTest() {
  describe('DeleteConfirmDialog', () => {
    it('should close dialog when click cancel', async () => {
      const driver = Driver.create()
      
      // 触发删除操作
      const deleteBtn = await driver.findComponent(ON.id('deleteButton'))
      await deleteBtn.click()
      
      // 验证弹窗显示
      const dialog = await driver.findComponent(ON.text('确认删除'))
      expect(await dialog.isVisible()).assertTrue()
      
      // 点击取消
      const cancelBtn = await driver.findComponent(ON.text('取消'))
      await cancelBtn.click()
      
      // 验证弹窗消失
      await driver.delayMs(300)
      expect(await dialog.isVisible()).assertFalse()
    })
  })
}

四、测试最佳实践

4.1 测试用例设计原则

  • 单一职责:每个测试用例只验证一个行为或场景
  • 可重复性:测试结果不依赖执行顺序或外部状态
  • 可读性:使用清晰的命名和注释,让测试意图一目了然
  • 独立性:测试用例之间互不依赖,可以并行执行

4.2 测试覆盖率目标

  • 核心业务逻辑:覆盖率 ≥ 80%
  • 工具类与公共模块:覆盖率 ≥ 90%
  • UI 关键路径:登录、支付、订单等核心流程必须有 UI 自动化测试

4.3 持续集成接入

将测试集成到 CI/CD 流程中,在代码提交或合并时自动运行:

bash 复制代码
# 命令行运行测试
hdc shell aa test -b com.example.app -m entry_test -s unittest OpenHarmonyTestRunner

# 在 CI 脚本中集成
npm run test
npm run test:ui

4.4 测试数据管理

  • 使用测试专用数据源:避免污染生产数据
  • 测试后清理数据 :在 afterEachafterAll 中重置状态
  • 使用数据驱动测试:通过参数化测试覆盖多种输入场景
typescript 复制代码
const testCases = [
  { input: 'user@example.com', expected: true },
  { input: 'invalid-email', expected: false },
  { input: '', expected: false }
]

testCases.forEach(({ input, expected }) => {
  it(`should validate email: ${input}`, () => {
    expect(StringUtil.isEmail(input)).assertEqual(expected)
  })
})

五、常见问题与解决方案

5.1 异步测试超时

问题:异步测试用例超时失败。

解决方案

  • 使用 done() 回调明确标记测试完成
  • 增加超时时间配置
  • 检查异步逻辑是否有未捕获的异常

5.2 UI 测试不稳定

问题:UI 测试偶现失败,无法稳定复现。

解决方案

  • 添加合理的 delayMs 等待动画或网络完成
  • 使用 waitForComponent 等待组件出现
  • 避免依赖绝对坐标,使用 idtext 定位

5.3 Mock 数据与真实环境差异

问题:Mock 测试通过,但真实环境出现问题。

解决方案

  • Mock 数据尽量贴近真实场景
  • 补充集成测试,使用真实接口验证
  • 定期更新 Mock 数据结构

六、总结

本文系统介绍了 HarmonyOS 应用的单元测试与 UI 自动化测试实战,涵盖测试框架、用例编写、Mock 技巧、UI 交互验证等核心内容。通过建立完整的测试体系,开发者可以显著提升代码质量、减少线上故障、加速迭代节奏。

测试不是开发的负担,而是质量的保障。在实际项目中,建议从核心业务逻辑的单元测试入手,逐步补充 UI 自动化测试覆盖关键路径,并将测试集成到 CI/CD 流程中,让测试成为开发流程的有机组成部分。

下一步,我们将探讨 HarmonyOS 应用的安全加固与数据保护实战,敬请期待。

相关推荐
郭邯1 小时前
用 Intl.DateTimeFormat 手写一个时区转换器,顺便聊聊我和 AI 结对编程的日常
前端·javascript
阿奇__1 小时前
微前端、iframe与Auth Code技术方案对比
前端
FungLeo1 小时前
React 管理后台实战 · 前端请求层怎么写?401 静默刷新、token 并发竞争、强制改密拦截一次说清
前端·react.js·前端框架
Vuji2 小时前
Pi 插件解剖|ssh.ts:只用 221 行,让 Agent 直接在远程机器干活
前端·人工智能·agent
漏刻有时2 小时前
Three.js 3D地图开发实录:从GeoJSON到流光飞线,一个纯前端SPA可视化引擎的12个坑
前端
clawdashi2 小时前
2026 办公 Agent 选型指南:按场景分五类,一次讲清
前端·人工智能
无糖可可果2 小时前
从零看懂 JWT 登录鉴权:一个 React Demo 的完整拆解
前端·javascript
驳是2 小时前
代码洁癖患者会喜欢的几款 CLI 小工具
前端
Csvn2 小时前
🛋️ requestIdleCallback:把非关键任务塞进浏览器的"空闲时间",告别卡顿
前端