测试指南
HarmonyOS 提供完整的测试能力,涵盖单元测试、UI 测试和性能测试。本文将详细介绍如何使用官方推荐的测试框架进行应用测试。
测试框架概览
HarmonyOS 主要使用以下测试框架:
| 框架 | 用途 | 适用场景 |
|---|---|---|
@ohos/hypium | 单元测试框架 | 逻辑函数、数据层、工具类测试 |
@ohos.UiTest | UI 自动化测试 | 页面交互、端到端流程测试 |
| DevEco Profiler | 性能测试 | 内存、CPU、FPS 等性能指标分析 |
前置条件
- 安装 DevEco Studio(已集成测试工具)
- 已创建 HarmonyOS 项目
- 真机或模拟器(UI 测试必须在设备上运行)
测试目录结构
DevEco Studio 默认在模块下创建 src/ohosTest/ 目录存放测试代码:
entry/src/
├── main/ # 主源码目录
│ ├── ets/
│ │ ├── entryability/
│ │ └── pages/
│ └── resources/
└── ohosTest/ # 测试代码目录
├── ets/
│ ├── test/ # 测试用例目录
│ │ ├── List.test.ets # 测试入口文件
│ │ ├── unit/ # 单元测试
│ │ │ ├── Math.test.ets
│ │ │ └── UserService.test.ets
│ │ └── ui/ # UI 测试
│ │ └── LoginPage.test.ets
│ └── testability/
│ └── TestAbility.ets
└── resources/提示
ohosTest/ets/test/是测试用例的默认存放位置List.test.ets是测试入口文件,用于聚合所有测试套件- 建议按
unit/和ui/分类组织测试文件
单元测试(@ohos/hypium)
基础测试用例
创建单元测试文件:
typescript
// entry/src/ohosTest/ets/test/unit/Math.test.ets
import { describe, it, expect } from '@ohos/hypium'
export default function mathTest() {
describe('数学运算测试', () => {
it('两数相加应返回正确结果', 0, () => {
const result = 1 + 1
expect(result).assertEqual(2)
})
it('边界值测试', 0, () => {
expect(0 + 0).assertEqual(0)
expect(Number.MAX_SAFE_INTEGER + 1).assertLarger(Number.MAX_SAFE_INTEGER)
})
})
}测试入口文件
在 List.test.ets 中聚合所有测试套件:
typescript
// entry/src/ohosTest/ets/test/List.test.ets
import mathTest from './unit/Math.test'
import userServiceTest from './unit/UserService.test'
import loginPageTest from './ui/LoginPage.test'
export default function testsuite() {
mathTest()
userServiceTest()
loginPageTest()
}异步测试
测试异步函数时使用 async/await:
typescript
// entry/src/ohosTest/ets/test/unit/Network.test.ets
import { describe, it, expect } from '@ohos/hypium'
import { http } from '@kit.NetworkKit'
export default function networkTest() {
describe('网络请求测试', () => {
it('GET 请求应返回 200', 0, async () => {
const request = http.createHttp()
try {
const response = await request.request('https://api.example.com/health', {
method: http.RequestMethod.GET
})
expect(response.responseCode).assertEqual(200)
} finally {
request.destroy()
}
})
it('超时请求应抛出异常', 0, async () => {
const request = http.createHttp()
try {
await request.request('https://api.example.com/slow', {
method: http.RequestMethod.GET,
connectTimeout: 100,
readTimeout: 100
})
expect(false).assertTrue() // 不应执行到这里
} catch (error) {
expect(error !== null).assertTrue()
} finally {
request.destroy()
}
})
})
}测试生命周期钩子
typescript
// entry/src/ohosTest/ets/test/unit/Database.test.ets
import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from '@ohos/hypium'
export default function databaseTest() {
describe('数据库操作测试', () => {
let db: DatabaseHelper
beforeAll(() => {
// 所有测试开始前执行一次
console.info('初始化数据库连接')
db = new DatabaseHelper()
})
beforeEach(() => {
// 每个测试用例开始前执行
db.clearTable('users')
})
afterEach(() => {
// 每个测试用例结束后执行
console.info('清理测试数据')
})
afterAll(() => {
// 所有测试结束后执行一次
db.close()
})
it('插入数据应成功', 0, () => {
const user = { id: 1, name: '张三' }
db.insert('users', user)
const result = db.query('users', 1)
expect(result.name).assertEqual('张三')
})
it('删除数据应成功', 0, () => {
db.insert('users', { id: 1, name: '张三' })
db.delete('users', 1)
const result = db.query('users', 1)
expect(result).assertNull()
})
})
}数据驱动测试
typescript
// entry/src/ohosTest/ets/test/unit/Validator.test.ets
import { describe, it, expect } from '@ohos/hypium'
export default function validatorTest() {
describe('输入验证测试', () => {
const testCases = [
{ input: 'user@example.com', expected: true, desc: '有效邮箱' },
{ input: 'invalid-email', expected: false, desc: '无效邮箱' },
{ input: '', expected: false, desc: '空字符串' },
{ input: 'a@b.c', expected: true, desc: '最短有效邮箱' }
]
testCases.forEach(({ input, expected, desc }) => {
it(`验证邮箱: ${desc}`, 0, () => {
const isValid = validateEmail(input)
expect(isValid).assertEqual(expected)
})
})
})
}
function validateEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return regex.test(email)
}UI 测试(@ohos.UiTest)
基础 UI 测试
typescript
// entry/src/ohosTest/ets/test/ui/LoginPage.test.ets
import { describe, it, expect } from '@ohos/hypium'
import { Driver, ON } from '@ohos.UiTest'
export default function loginPageTest() {
describe('登录页面 UI 测试', () => {
it('输入正确账号密码应登录成功', 0, async () => {
// 创建 UI 驱动实例
const driver = Driver.create()
// 等待页面加载
await driver.delayMs(1000)
// 查找账号输入框并输入
const usernameInput = await driver.findComponent(ON.id('username_input'))
await usernameInput.inputText('testuser')
// 查找密码输入框并输入
const passwordInput = await driver.findComponent(ON.id('password_input'))
await passwordInput.inputText('password123')
// 点击登录按钮
const loginBtn = await driver.findComponent(ON.text('登录'))
await loginBtn.click()
// 等待响应
await driver.delayMs(1000)
// 验证登录成功提示
const successTip = await driver.findComponent(ON.text('登录成功'))
expect(successTip !== null).assertTrue()
})
})
}组件匹配器(ON)
@ohos.UiTest 提供多种组件匹配方式:
typescript
import { Driver, ON } from '@ohos.UiTest'
const driver = Driver.create()
// 按文本匹配
const btnByText = await driver.findComponent(ON.text('确认'))
// 按 ID 匹配(需在组件上设置 .id('xxx'))
const btnById = await driver.findComponent(ON.id('confirmBtn'))
// 按组件类型匹配
const allButtons = await driver.findComponents(ON.type('Button'))
// 按是否可点击匹配
const clickableBtn = await driver.findComponent(ON.clickable(true))
// 按是否启用匹配
const enabledInput = await driver.findComponent(ON.enabled(true))
// 组合条件匹配
const submitBtn = await driver.findComponent(
ON.text('提交').and(ON.clickable(true))
)
// 按文本包含匹配
const partialMatch = await driver.findComponent(ON.textContains('部分文本'))常见 UI 操作
typescript
import { describe, it, expect } from '@ohos/hypium'
import { Driver, ON, Component } from '@ohos.UiTest'
export default function uiActionsTest() {
describe('UI 操作测试', () => {
let driver: Driver
beforeAll(async () => {
driver = Driver.create()
await driver.delayMs(500)
})
it('滑动列表测试', 0, async () => {
// 查找列表组件
const list = await driver.findComponent(ON.type('List'))
// 向上滑动
await list.scrollSearch(
ON.text('目标项'),
500, // 滑动距离
10 // 最大滑动次数
)
// 验证目标项可见
const targetItem = await driver.findComponent(ON.text('目标项'))
expect(targetItem !== null).assertTrue()
})
it('弹窗交互测试', 0, async () => {
// 触发弹窗
const triggerBtn = await driver.findComponent(ON.text('删除'))
await triggerBtn.click()
// 等待弹窗出现
await driver.delayMs(300)
// 点击确认
const confirmBtn = await driver.findComponent(ON.text('确认删除'))
await confirmBtn.click()
// 验证弹窗消失
await driver.delayMs(300)
const dialog = await driver.findComponent(ON.type('Dialog'))
expect(dialog === null).assertTrue()
})
it('获取组件属性测试', 0, async () => {
const textComponent = await driver.findComponent(ON.id('status_text'))
// 获取文本内容
const text = await textComponent.getText()
expect(text).assertEqual('已完成')
// 获取组件边界
const bounds = await textComponent.getBounds()
expect(bounds.left >= 0).assertTrue()
expect(bounds.top >= 0).assertTrue()
})
it('截图对比测试', 0, async () => {
// 截取当前屏幕
await driver.screenCap('/data/screenshot.png')
// 验证截图文件存在
const fs = fileIo.openSync('/data/screenshot.png', fileIo.OpenMode.READ_ONLY)
const stat = fileIo.fstatSync(fs.fd)
expect(stat.size > 0).assertTrue()
fileIo.closeSync(fs)
})
})
}页面跳转测试
typescript
// entry/src/ohosTest/ets/test/ui/Navigation.test.ets
import { describe, it, expect } from '@ohos/hypium'
import { Driver, ON } from '@ohos.UiTest'
export default function navigationTest() {
describe('页面导航测试', () => {
it('点击列表项应跳转到详情页', 0, async () => {
const driver = Driver.create()
// 等待首页加载
await driver.delayMs(1000)
// 点击第一个列表项
const firstItem = await driver.findComponent(ON.id('item_0'))
await firstItem.click()
// 等待详情页加载
await driver.delayMs(800)
// 验证详情页标题
const title = await driver.findComponent(ON.id('detail_title'))
const titleText = await title.getText()
expect(titleText.length > 0).assertTrue()
// 点击返回
const backBtn = await driver.findComponent(ON.id('back_button'))
await backBtn.click()
// 验证回到首页
await driver.delayMs(500)
const homeTitle = await driver.findComponent(ON.text('首页'))
expect(homeTitle !== null).assertTrue()
})
})
}断言 API 完整列表
@ohos/hypium 提供丰富的断言方法:
基础断言
| 断言方法 | 说明 | 示例 |
|---|---|---|
assertEqual(expected) | 严格相等 | expect(a).assertEqual(2) |
assertTrue() | 值为 true | expect(flag).assertTrue() |
assertFalse() | 值为 false | expect(flag).assertFalse() |
assertNull() | 值为 null | expect(obj).assertNull() |
assertUndefined() | 值为 undefined | expect(val).assertUndefined() |
assertNaN() | 值为 NaN | expect(val).assertNaN() |
数值比较断言
| 断言方法 | 说明 | 示例 |
|---|---|---|
assertLarger(base) | 大于 | expect(a).assertLarger(10) |
assertLess(base) | 小于 | expect(a).assertLess(100) |
assertLargerOrEqual(base) | 大于等于 | expect(a).assertLargerOrEqual(0) |
assertLessOrEqual(base) | 小于等于 | expect(a).assertLessOrEqual(100) |
assertClose(expected, delta) | 近似相等 | expect(3.14).assertClose(3.14159, 0.01) |
字符串/数组断言
| 断言方法 | 说明 | 示例 |
|---|---|---|
assertContain(container, item) | 包含元素 | expect(arr).assertContain(arr, item) |
assertContain(str, substr) | 包含子串 | expect(str).assertContain(str, 'abc') |
assertFail(message?) | 强制失败 | expect().assertFail('不应执行到这里') |
类型与实例断言
| 断言方法 | 说明 | 示例 |
|---|---|---|
assertInstanceOf(type) | 实例类型 | expect(obj).assertInstanceOf('User') |
assertThrow() | 抛出异常 | expect(() => risky()).assertThrow() |
assertPromiseIsResolved(promise) | Promise 已 resolve | expect(p).assertPromiseIsResolved(p) |
使用示例
typescript
import { describe, it, expect } from '@ohos/hypium'
export default function assertionDemoTest() {
describe('断言 API 演示', () => {
it('基础断言', 0, () => {
expect(1 + 1).assertEqual(2)
expect(true).assertTrue()
expect(false).assertFalse()
expect(null).assertNull()
})
it('数值比较', 0, () => {
expect(100).assertLarger(50)
expect(50).assertLess(100)
expect(50).assertLargerOrEqual(50)
expect(3.14159).assertClose(3.14, 0.01)
})
it('字符串和数组', 0, () => {
const arr = [1, 2, 3]
expect(arr).assertContain(arr, 2)
const str = 'Hello HarmonyOS'
expect(str).assertContain(str, 'HarmonyOS')
})
it('异常断言', 0, () => {
expect(() => {
throw new Error('测试异常')
}).assertThrow()
expect(() => {
// 不会抛出异常
}).assertThrow() // 此断言会失败
})
it('类型断言', 0, () => {
class User {
name: string = ''
}
const user = new User()
expect(user).assertInstanceOf('User')
})
})
}运行测试的方法
在 DevEco Studio 中运行
方式一:运行全部测试
- 点击顶部工具栏的运行配置下拉框
- 选择
entry_test或ohosTest配置 - 点击运行按钮(绿色三角形)
方式二:运行单个测试文件
- 在 Project 视图中找到测试文件(如
Math.test.ets) - 右键点击文件
- 选择
Run 'Math.test'
方式三:运行单个测试用例
- 打开测试文件
- 点击某个
it()左侧的绿色播放图标 - 选择
Run '具体测试名称'
命令行运行
bash
# 运行所有测试
hvigorw test
# 运行指定模块的测试
hvigorw --mode module -p module=entry@ohosTest test
# 运行特定测试文件
hvigorw test -p testFile=Math.test.ets
# 带覆盖率报告
hvigorw test --coverage查看测试结果
测试运行后,结果会显示在:
- Run 面板:显示每个测试用例的通过/失败状态
- Test 面板:可视化测试树,绿色表示通过,红色表示失败
- 控制台输出:详细的断言失败信息和堆栈跟踪
Test Results:
✓ 数学运算测试 - 两数相加应返回正确结果 (12ms)
✓ 数学运算测试 - 边界值测试 (5ms)
✗ 网络请求测试 - GET 请求应返回 200 (1500ms)
Expected: 200
Actual: 404
at Network.test.ets:15测试最佳实践
1. 测试命名规范
typescript
// 好的命名:描述行为而非实现
describe('用户服务', () => {
it('当邮箱格式正确时应返回 true', 0, () => { })
it('当用户不存在时应抛出 NotFoundError', 0, () => { })
})
// 避免的命名
describe('UserService', () => {
it('test1', 0, () => { }) // 太模糊
it('验证功能', 0, () => { }) // 太笼统
})2. 单一职责原则
每个测试只验证一个概念:
typescript
// 好的做法:每个测试一个断言主题
it('有效邮箱应通过验证', 0, () => {
expect(validateEmail('user@example.com')).assertTrue()
})
it('无效邮箱应拒绝验证', 0, () => {
expect(validateEmail('invalid')).assertFalse()
})
// 避免:一个测试验证太多东西
it('验证各种邮箱', 0, () => {
expect(validateEmail('a@b.com')).assertTrue()
expect(validateEmail('invalid')).assertFalse()
expect(validateEmail('')).assertFalse()
// ... 太多断言
})3. 使用 beforeEach 保持测试独立
typescript
describe('购物车服务', () => {
let cart: ShoppingCart
beforeEach(() => {
// 每个测试前创建新的实例,避免状态污染
cart = new ShoppingCart()
})
it('添加商品后应包含该商品', 0, () => {
cart.addItem({ id: 1, name: '商品A' })
expect(cart.getItems().length).assertEqual(1)
})
it('新购物车应为空', 0, () => {
// 不受上一个测试影响
expect(cart.getItems().length).assertEqual(0)
})
})4. 测试数据与生产数据分离
typescript
// 使用测试专用的配置
const TEST_CONFIG = {
apiBaseUrl: 'https://test-api.example.com',
timeout: 5000
}
// 或使用 mock 数据
class MockUserService implements IUserService {
async getUser(id: number): Promise<User> {
return { id, name: '测试用户', email: 'test@example.com' }
}
}5. 测试覆盖率目标
| 层级 | 建议覆盖率 | 说明 |
|---|---|---|
| 工具函数/纯逻辑 | 90%+ | 无副作用,容易测试 |
| 数据层/服务层 | 80%+ | 核心业务逻辑 |
| UI 层 | 60%+ | 关键流程覆盖 |
| 第三方集成 | 50%+ | 主要路径覆盖 |
6. 测试文件组织
ets/
├── main/
│ ├── pages/
│ │ └── LoginPage.ets
│ └── services/
│ └── UserService.ets
└── ohosTest/
└── ets/test/
├── List.test.ets # 测试入口
├── unit/
│ ├── UserService.test.ets # 对应 main/services/UserService.ets
│ └── HttpUtil.test.ets
└── ui/
├── LoginPage.test.ets # 对应 main/pages/LoginPage.ets
└── HomePage.test.ets7. 常见陷阱与避免方法
typescript
// 陷阱 1:测试间有依赖
// 避免:使用 beforeEach 重置状态
// 陷阱 2:不清理资源
// 避免:使用 try/finally 或 afterEach
it('网络请求测试', 0, async () => {
const request = http.createHttp()
try {
// 测试逻辑
} finally {
request.destroy() // 必须清理
}
})
// 陷阱 3:忽略异步错误
// 避免:始终 await Promise
it('异步操作', 0, async () => {
await someAsyncOperation() // 不要忘记 await
})
// 陷阱 4:硬编码等待时间
// 避免:使用轮询或事件等待
// 好的做法:使用 driver.findComponent 的隐式等待
// 避免:await driver.delayMs(5000) // 固定等待