Skip to content

测试指南

HarmonyOS 提供完整的测试能力,涵盖单元测试、UI 测试和性能测试。本文将详细介绍如何使用官方推荐的测试框架进行应用测试。

测试框架概览

HarmonyOS 主要使用以下测试框架:

框架用途适用场景
@ohos/hypium单元测试框架逻辑函数、数据层、工具类测试
@ohos.UiTestUI 自动化测试页面交互、端到端流程测试
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()值为 trueexpect(flag).assertTrue()
assertFalse()值为 falseexpect(flag).assertFalse()
assertNull()值为 nullexpect(obj).assertNull()
assertUndefined()值为 undefinedexpect(val).assertUndefined()
assertNaN()值为 NaNexpect(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 已 resolveexpect(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 中运行

方式一:运行全部测试

  1. 点击顶部工具栏的运行配置下拉框
  2. 选择 entry_testohosTest 配置
  3. 点击运行按钮(绿色三角形)

方式二:运行单个测试文件

  1. 在 Project 视图中找到测试文件(如 Math.test.ets
  2. 右键点击文件
  3. 选择 Run 'Math.test'

方式三:运行单个测试用例

  1. 打开测试文件
  2. 点击某个 it() 左侧的绿色播放图标
  3. 选择 Run '具体测试名称'

命令行运行

bash
# 运行所有测试
hvigorw test

# 运行指定模块的测试
hvigorw --mode module -p module=entry@ohosTest test

# 运行特定测试文件
hvigorw test -p testFile=Math.test.ets

# 带覆盖率报告
hvigorw test --coverage

查看测试结果

测试运行后,结果会显示在:

  1. Run 面板:显示每个测试用例的通过/失败状态
  2. Test 面板:可视化测试树,绿色表示通过,红色表示失败
  3. 控制台输出:详细的断言失败信息和堆栈跟踪
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.ets

7. 常见陷阱与避免方法

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) // 固定等待

下一步