HarmonyOS鸿蒙Next应用开发中如何编写单元测试?
HarmonyOS鸿蒙Next应用开发中如何编写单元测试?
1.在HarmonyOS应用开发中如何编写单元测试?
2.如何进行UI自动化测试和代码质量保障?
3 回复
解决方案
1. 基础单元测试
// tests/ExampleTest.test.ets
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '[@ohos](/user/ohos)/hypium'
export default function exampleTest() {
describe('基础单元测试', () => {
// 所有测试前执行一次
beforeAll(() => {
console.log('测试套件开始')
})
// 所有测试后执行一次
afterAll(() => {
console.log('测试套件结束')
})
// 每个测试前执行
beforeEach(() => {
console.log('测试用例开始')
})
// 每个测试后执行
afterEach(() => {
console.log('测试用例结束')
})
// 测试用例
it('should_add_two_numbers', () => {
const result = add(2, 3)
expect(result).assertEqual(5)
})
it('should_multiply_two_numbers', () => {
const result = multiply(4, 5)
expect(result).assertEqual(20)
})
it('should_throw_error_for_division_by_zero', () => {
expect(() => {
divide(10, 0)
}).assertThrow('Cannot divide by zero')
})
})
}
// 被测试的函数
function add(a: number, b: number): number {
return a + b
}
function multiply(a: number, b: number): number {
return a * b
}
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Cannot divide by zero')
}
return a / b
}
2. 工具类测试
// utils/StringUtils.ets
export class StringUtils {
static isEmpty(str: string | null | undefined): boolean {
return str === null || str === undefined || str.trim() === ''
}
static capitalize(str: string): string {
if (this.isEmpty(str)) return ''
return str.charAt(0).toUpperCase() + str.slice(1)
}
static truncate(str: string, length: number): string {
if (str.length <= length) return str
return str.substring(0, length) + '...'
}
static reverse(str: string): string {
return str.split('').reverse().join('')
}
}
// tests/StringUtils.test.ets
import { describe, it, expect } from '[@ohos](/user/ohos)/hypium'
import { StringUtils } from '../utils/StringUtils'
export default function stringUtilsTest() {
describe('StringUtils测试', () => {
describe('isEmpty方法', () => {
it('should_return_true_for_null', () => {
expect(StringUtils.isEmpty(null)).assertTrue()
})
it('should_return_true_for_undefined', () => {
expect(StringUtils.isEmpty(undefined)).assertTrue()
})
it('should_return_true_for_empty_string', () => {
expect(StringUtils.isEmpty('')).assertTrue()
})
it('should_return_true_for_whitespace', () => {
expect(StringUtils.isEmpty(' ')).assertTrue()
})
it('should_return_false_for_valid_string', () => {
expect(StringUtils.isEmpty('hello')).assertFalse()
})
})
describe('capitalize方法', () => {
it('should_capitalize_first_letter', () => {
expect(StringUtils.capitalize('hello')).assertEqual('Hello')
})
it('should_return_empty_for_empty_string', () => {
expect(StringUtils.capitalize('')).assertEqual('')
})
it('should_handle_single_char', () => {
expect(StringUtils.capitalize('a')).assertEqual('A')
})
})
describe('truncate方法', () => {
it('should_truncate_long_string', () => {
const result = StringUtils.truncate('Hello World', 5)
expect(result).assertEqual('Hello...')
})
it('should_not_truncate_short_string', () => {
const result = StringUtils.truncate('Hi', 5)
expect(result).assertEqual('Hi')
})
})
describe('reverse方法', () => {
it('should_reverse_string', () => {
expect(StringUtils.reverse('hello')).assertEqual('olleh')
})
it('should_handle_empty_string', () => {
expect(StringUtils.reverse('')).assertEqual('')
})
})
})
}
3. ViewModel测试
// viewmodel/UserViewModel.ets
export class UserViewModel {
private username: string = ''
private email: string = ''
setUsername(value: string): void {
this.username = value
}
setEmail(value: string): void {
this.email = value
}
getUsername(): string {
return this.username
}
getEmail(): string {
return this.email
}
validate(): { valid: boolean, errors: string[] } {
const errors: string[] = []
if (!this.username || this.username.length < 3) {
errors.push('用户名至少3个字符')
}
if (!this.email || !this.isValidEmail(this.email)) {
errors.push('邮箱格式不正确')
}
return {
valid: errors.length === 0,
errors: errors
}
}
private isValidEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return regex.test(email)
}
clear(): void {
this.username = ''
this.email = ''
}
}
// tests/UserViewModel.test.ets
import { describe, it, expect, beforeEach } from '[@ohos](/user/ohos)/hypium'
import { UserViewModel } from '../viewmodel/UserViewModel'
export default function userViewModelTest() {
describe('UserViewModel测试', () => {
let viewModel: UserViewModel
beforeEach(() => {
viewModel = new UserViewModel()
})
describe('设置和获取用户名', () => {
it('should_set_and_get_username', () => {
viewModel.setUsername('testuser')
expect(viewModel.getUsername()).assertEqual('testuser')
})
})
describe('设置和获取邮箱', () => {
it('should_set_and_get_email', () => {
viewModel.setEmail('test@example.com')
expect(viewModel.getEmail()).assertEqual('test@example.com')
})
})
describe('数据验证', () => {
it('should_fail_validation_for_short_username', () => {
viewModel.setUsername('ab')
viewModel.setEmail('test@example.com')
const result = viewModel.validate()
expect(result.valid).assertFalse()
expect(result.errors.length).assertEqual(1)
expect(result.errors[0]).assertEqual('用户名至少3个字符')
})
it('should_fail_validation_for_invalid_email', () => {
viewModel.setUsername('testuser')
viewModel.setEmail('invalid-email')
const result = viewModel.validate()
expect(result.valid).assertFalse()
expect(result.errors.length).assertEqual(1)
expect(result.errors[0]).assertEqual('邮箱格式不正确')
})
it('should_pass_validation_for_valid_data', () => {
viewModel.setUsername('testuser')
viewModel.setEmail('test@example.com')
const result = viewModel.validate()
expect(result.valid).assertTrue()
expect(result.errors.length).assertEqual(0)
})
})
describe('清空数据', () => {
it('should_clear_all_data', () => {
viewModel.setUsername('testuser')
viewModel.setEmail('test@example.com')
viewModel.clear()
expect(viewModel.getUsername()).assertEqual('')
expect(viewModel.getEmail()).assertEqual('')
})
})
})
}
4. Mock数据测试
// service/UserService.ets
import http from '[@ohos](/user/ohos).net.http'
export interface User {
id: number
name: string
email: string
}
export class UserService {
private baseUrl = 'https://api.example.com'
async getUser(id: number): Promise<User> {
const httpRequest = http.createHttp()
try {
const response = await httpRequest.request(`${this.baseUrl}/users/${id}`)
return JSON.parse(response.result as string) as User
} finally {
httpRequest.destroy()
}
}
async createUser(user: Omit<User, 'id'>): Promise<User> {
const httpRequest = http.createHttp()
try {
const response = await httpRequest.request(
`${this.baseUrl}/users`,
{
method: http.RequestMethod.POST,
extraData: JSON.stringify(user)
}
)
return JSON.parse(response.result as string) as User
} finally {
httpRequest.destroy()
}
}
}
// tests/UserService.test.ets
import { describe, it, expect, beforeEach } from '[@ohos](/user/ohos)/hypium'
import { UserService, User } from '../service/UserService'
// Mock数据
const mockUser: User = {
id: 1,
name: 'Test User',
email: 'test@example.com'
}
export default function userServiceTest() {
describe('UserService测试', () => {
let service: UserService
beforeEach(() => {
service = new UserService()
})
// 注意: 实际测试需要Mock网络请求
// 这里仅作示例,实际应使用Mock框架
it('should_get_user_by_id', async () => {
// 在实际测试中,应Mock http请求
// 这里假设已经Mock了响应
// const user = await service.getUser(1)
// expect(user.id).assertEqual(mockUser.id)
// expect(user.name).assertEqual(mockUser.name)
// expect(user.email).assertEqual(mockUser.email)
})
it('should_create_user', async () => {
// const newUser = {
// name: 'New User',
// email: 'new@example.com'
// }
// const created = await service.createUser(newUser)
// expect(created.id).assertLarger(0)
// expect(created.name).assertEqual(newUser.name)
// expect(created.email).assertEqual(newUser.email)
})
})
}
5. 测试工具类
// tests/TestUtil.ets
export class TestUtil {
/**
* 创建Mock数据
*/
static createMockArray<T>(count: number, factory: (index: number) => T): T[] {
return Array.from({ length: count }, (_, i) => factory(i))
}
/**
* 等待异步完成
*/
static async waitFor(
condition: () => boolean,
timeout: number = 5000,
interval: number = 100
): Promise<void> {
const startTime = Date.now()
while (!condition()) {
if (Date.now() - startTime > timeout) {
throw new Error('等待超时')
}
await this.delay(interval)
}
}
/**
* 延时
*/
static delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* 断言数组包含
*/
static assertArrayContains<T>(array: T[], item: T): void {
if (!array.includes(item)) {
throw new Error(`数组不包含元素: ${item}`)
}
}
/**
* 断言对象相等
*/
static assertObjectEquals(obj1: Object, obj2: Object): void {
const json1 = JSON.stringify(obj1)
const json2 = JSON.stringify(obj2)
if (json1 !== json2) {
throw new Error(`对象不相等:\n期望: ${json2}\n实际: ${json1}`)
}
}
/**
* 生成随机字符串
*/
static randomString(length: number = 10): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
/**
* 生成随机数字
*/
static randomNumber(min: number = 0, max: number = 100): number {
return Math.floor(Math.random() * (max - min + 1)) + min
}
}
// 使用示例
import { describe, it, expect } from '[@ohos](/user/ohos)/hypium'
export default function testUtilDemo() {
describe('TestUtil示例', () => {
it('should_create_mock_array', () => {
const mockUsers = TestUtil.createMockArray(5, (i) => ({
id: i,
name: `User${i}`,
email: `user${i}@example.com`
}))
expect(mockUsers.length).assertEqual(5)
expect(mockUsers[0].name).assertEqual('User0')
})
it('should_wait_for_condition', async () => {
let flag = false
setTimeout(() => {
flag = true
}, 1000)
await TestUtil.waitFor(() => flag)
expect(flag).assertTrue()
})
it('should_generate_random_string', () => {
const str1 = TestUtil.randomString(10)
const str2 = TestUtil.randomString(10)
expect(str1.length).assertEqual(10)
expect(str2.length).assertEqual(10)
expect(str1).assertNotEqual(str2)
})
})
}
关键要点
- 测试框架: 使用@ohos/hypium进行单元测试
- 测试结构: describe组织测试套件,it定义测试用例
- 断言: 使用expect进行结果验证
- 生命周期: beforeEach/afterEach管理测试环境
- Mock数据: 隔离外部依赖,使用Mock数据
最佳实践
- 命名规范: 测试用例名称清晰描述测试内容
- 独立性: 每个测试用例独立,互不影响
- 覆盖率: 关注边界条件和异常情况
- 可维护: 测试代码同样需要良好的结构
- 持续集成: 自动化运行测试,及时发现问题
更多关于HarmonyOS鸿蒙Next应用开发中如何编写单元测试?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
在HarmonyOS Next中,单元测试使用ArkTS编写,基于ArkUI Test框架。测试文件通常命名为xxx.test.ets,与源码同目录。使用@Test装饰器标记测试用例,@BeforeEach和@AfterEach用于前置后置操作。断言方法如assert.equal()来自hilog或@ohos.util。通过DevEco Studio的测试运行器执行。
在HarmonyOS Next应用开发中,编写单元测试主要依赖于ArkTS/ArkUI框架提供的测试能力。以下是核心方法:
-
单元测试编写:
- 使用
@Test装饰器标记测试用例,通常与describe、it或beforeEach等函数结合组织测试套件。 - 通过
ohos.application.testRunner模块的TestRunnerAPI运行测试,支持异步测试和断言库(如expect)。 - 示例:
import { describe, it, expect } from '@ohos/hypium'; describe('CalculatorTest', () => { it('add_test', () => { expect(1 + 1).assertEqual(2); }); });
- 使用
-
UI自动化测试:
- 使用
UiTest框架,通过DriverAPI模拟用户操作(如点击、滑动)。 - 结合
Component或Window对象定位UI元素,支持属性检查和事件触发。 - 示例:
import { Driver } from '@ohos.uitest'; let driver = Driver.create(); await driver.delayMs(1000); let button = await driver.findComponent(By.text('Submit')); await button.click();
- 使用
-
代码质量保障:
- 集成静态检查工具(如
ESLint)规范代码风格。 - 利用
ohos.previewer或DevEco Studio的调试工具进行实时验证。 - 结合持续集成(CI)自动执行测试,确保代码稳定性。
- 集成静态检查工具(如
测试文件需放在模块的ohosTest目录下,并通过DevEco Studio的测试面板或命令行执行。

