HarmonyOS开发:异步测试与Promise测试
HarmonyOS开发:异步测试与Promise测试
📌 核心要点:异步代码的测试不是"加个await就完事了"——你得搞清楚Promise什么时候resolve、超时怎么处理、并发异步怎么控制。Hypium提供了async/await和done回调两种模式,选对了模式,异步测试才能稳。
一、背景与动机
你的业务代码里有异步操作吗?网络请求、文件读写、定时器、数据库查询——哪个不是异步的?但你的测试呢?是不是还在写同步断言,然后祈祷异步操作在断言执行前完成?
这种测试,本地跑也许能过(因为开发机性能好,异步操作很快就返回了),但到了CI环境,网络慢一点、CPU忙一点,异步操作还没返回,断言已经执行了——测试挂了。而且这种挂法是随机的,时过时不过,排查起来能让人崩溃。
异步测试的核心挑战就一个:怎么确保断言在异步操作完成后才执行。听起来简单,做起来坑不少。Promise链断了、超时没处理、并发异步竞态……每一个都能让你的测试变成"薛定谔的通过"。
二、核心原理
2.1 异步测试模式对比
flowchart TB
A[异步测试模式] --> B[async/await模式]
A --> C[done回调模式]
A --> D[Promise返回模式]
B --> B1[最常用]
B --> B2[代码可读性高]
B --> B3[错误自动传播]
C --> C1[兼容旧代码]
C --> C2[灵活控制时机]
C --> C3[容易忘记调用done]
D --> D1[最简洁]
D --> D2[框架自动等待resolve]
D --> D3[不支持复杂流程]
classDef mainStyle fill:#4CAF50,stroke:#388E3C,color:#fff,font-weight:bold
classDef asyncStyle fill:#2196F3,stroke:#1976D2,color:#fff
classDef doneStyle fill:#FF9800,stroke:#F57C00,color:#fff
classDef promiseStyle fill:#9C27B0,stroke:#7B1FA2,color:#fff
class A mainStyle
class B,B1,B2,B3 asyncStyle
class C,C1,C2,C3 doneStyle
class D,D1,D2,D3 promiseStyle
2.2 异步测试执行时序
同步测试:it → 断言 → 结束(线性执行)
异步测试:it → 发起异步操作 → [等待] → 异步完成 → 断言 → 结束
关键区别在于"等待"这一步。Hypium怎么知道你的异步操作完成了?三种方式告诉它:
- async/await:用
async标记测试函数,await等待Promise resolve,框架自动处理 - done回调:测试函数接收一个
done参数,你手动调用done()表示完成 - 返回Promise:测试函数直接返回Promise,框架等它resolve
2.3 超时机制
异步操作可能永远不返回(网络断开、死锁、Bug)。没有超时机制,测试就会永远挂起。Hypium为每个异步测试设置了默认超时时间(通常5秒),超时后自动标记为失败。
| 场景 | 建议超时时间 |
|---|---|
| 本地数据操作 | 1-3秒 |
| 网络请求 | 5-10秒 |
| 文件读写 | 3-5秒 |
| 复杂计算 | 根据实际耗时设置 |
三、代码实战
3.1 基础用法:async/await测试模式
// AsyncDataService.ets - 异步数据服务
export interface DataResult {
success: boolean
data?: unknown
error?: string
timestamp: number
}
export class AsyncDataService {
private cache: Map<string, unknown> = new Map()
// 模拟异步数据获取
async fetchData(key: string, delay: number = 100): Promise<DataResult> {
return new Promise((resolve) => {
setTimeout(() => {
if (this.cache.has(key)) {
resolve({
success: true,
data: this.cache.get(key),
timestamp: Date.now()
})
} else {
resolve({
success: false,
error: `数据不存在: ${key}`,
timestamp: Date.now()
})
}
}, delay)
})
}
// 模拟异步数据保存
async saveData(key: string, value: unknown, delay: number = 100): Promise<DataResult> {
return new Promise((resolve) => {
setTimeout(() => {
this.cache.set(key, value)
resolve({
success: true,
data: value,
timestamp: Date.now()
})
}, delay)
})
}
// 模拟可能失败的异步操作
async riskyOperation(shouldFail: boolean, delay: number = 100): Promise<DataResult> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error('操作失败'))
} else {
resolve({
success: true,
data: '操作成功',
timestamp: Date.now()
})
}
}, delay)
})
}
// 模拟超时操作
async slowOperation(delay: number = 10000): Promise<DataResult> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
success: true,
data: '终于完成了',
timestamp: Date.now()
})
}, delay)
})
}
// 串行异步操作
async sequentialFetch(keys: string[]): Promise<DataResult[]> {
const results: DataResult[] = []
for (const key of keys) {
const result = await this.fetchData(key, 50)
results.push(result)
}
return results
}
// 并行异步操作
async parallelFetch(keys: string[]): Promise<DataResult[]> {
const promises = keys.map(key => this.fetchData(key, 50))
return Promise.all(promises)
}
clearCache(): void {
this.cache.clear()
}
}
// AsyncDataServiceTest.ets - async/await基础测试
import { describe, it, beforeEach } from '@ohos/hypium'
import { AsyncDataService } from '../../../main/ets/service/AsyncDataService'
export default function asyncDataServiceTest() {
describe('AsyncDataService异步数据服务', () => {
let service: AsyncDataService
beforeEach(() => {
service = new AsyncDataService()
})
describe('fetchData异步获取', () => {
it('fetchData_缓存中有数据_返回成功', 0, async () => {
// 先存数据
await service.saveData('test_key', 'test_value')
// 再取数据
const result = await service.fetchData('test_key')
assertEqual(result.success, true)
assertEqual(result.data, 'test_value')
})
it('fetchData_缓存中无数据_返回失败', 0, async () => {
const result = await service.fetchData('nonexistent')
assertEqual(result.success, false)
assertEqual(result.error, '数据不存在: nonexistent')
})
it('fetchData_返回结果包含时间戳', 0, async () => {
const beforeTime = Date.now()
await service.saveData('time_test', 'value')
const result = await service.fetchData('time_test')
const afterTime = Date.now()
assertTrue(result.timestamp >= beforeTime)
assertTrue(result.timestamp <= afterTime)
})
})
describe('saveData异步保存', () => {
it('saveData_保存成功_返回数据', 0, async () => {
const result = await service.saveData('new_key', { name: '测试' })
assertEqual(result.success, true)
assertEqual(result.data.name, '测试')
})
it('saveData_保存后可获取', 0, async () => {
await service.saveData('verify_key', 42)
const fetchResult = await service.fetchData('verify_key')
assertEqual(fetchResult.success, true)
assertEqual(fetchResult.data, 42)
})
})
describe('riskyOperation可能失败的操作', () => {
it('riskyOperation_成功路径_返回结果', 0, async () => {
const result = await service.riskyOperation(false)
assertEqual(result.success, true)
})
it('riskyOperation_失败路径_抛出异常', 0, async () => {
try {
await service.riskyOperation(true)
assertTrue(false, '应该抛出异常但没有')
} catch (e) {
assertEqual((e as Error).message, '操作失败')
}
})
})
})
}
3.2 进阶用法:done回调与超时处理
// EventEmitter.ets - 事件发射器(回调风格)
export type EventCallback = (data?: unknown) => void
export class EventEmitter {
private listeners: Map<string, EventCallback[]> = new Map()
on(event: string, callback: EventCallback): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, [])
}
this.listeners.get(event)!.push(callback)
}
off(event: string, callback: EventCallback): void {
const callbacks = this.listeners.get(event)
if (callbacks) {
const index = callbacks.indexOf(callback)
if (index !== -1) {
callbacks.splice(index, 1)
}
}
}
emit(event: string, data?: unknown): void {
const callbacks = this.listeners.get(event)
if (callbacks) {
callbacks.forEach(cb => cb(data))
}
}
// 延迟发射事件
emitDelayed(event: string, delay: number, data?: unknown): void {
setTimeout(() => {
this.emit(event, data)
}, delay)
}
removeAllListeners(): void {
this.listeners.clear()
}
}
// EventEmitterTest.ets - done回调和超时处理
import { describe, it, beforeEach, afterEach } from '@ohos/hypium'
import { EventEmitter } from '../../../main/ets/utils/EventEmitter'
export default function eventEmitterTest() {
describe('EventEmitter事件发射器', () => {
let emitter: EventEmitter
beforeEach(() => {
emitter = new EventEmitter()
})
afterEach(() => {
emitter.removeAllListeners()
})
describe('同步事件', () => {
it('emit_触发事件_回调执行', 0, () => {
let received = ''
emitter.on('test', (data) => {
received = data as string
})
emitter.emit('test', 'hello')
assertEqual(received, 'hello')
})
it('emit_多个监听器_全部执行', 0, () => {
let count = 0
emitter.on('test', () => { count++ })
emitter.on('test', () => { count++ })
emitter.emit('test')
assertEqual(count, 2)
})
})
describe('异步事件(done回调模式)', () => {
it('emitDelayed_延迟触发_done标记完成', 0, (done: Function) => {
emitter.on('delayed', (data) => {
assertEqual(data, 'delayed_data')
done() // 通知框架:异步操作完成
})
emitter.emitDelayed('delayed', 100, 'delayed_data')
// 如果done()没被调用,测试会超时失败
})
it('emitDelayed_多个延迟事件_按序触发', 0, (done: Function) => {
const results: string[] = []
emitter.on('event1', () => {
results.push('event1')
})
emitter.on('event2', () => {
results.push('event2')
// 最后一个事件触发后才调用done
assertEqual(results.length, 2)
assertEqual(results[0], 'event1')
assertEqual(results[1], 'event2')
done()
})
emitter.emitDelayed('event1', 50)
emitter.emitDelayed('event2', 150)
})
it('emitDelayed_超时未触发_测试失败', 0, (done: Function) => {
// 设置超时:如果3秒内没有触发事件,测试失败
const timeoutId = setTimeout(() => {
done(new Error('事件超时未触发'))
}, 3000)
emitter.on('timeout_test', () => {
clearTimeout(timeoutId)
done()
})
// 不发射事件,让超时逻辑生效
// 实际测试中应该发射事件,这里演示超时处理
emitter.emitDelayed('timeout_test', 200)
})
})
describe('off移除监听', () => {
it('off_移除后不再触发', 0, () => {
let count = 0
const callback = () => { count++ }
emitter.on('test', callback)
emitter.emit('test')
assertEqual(count, 1)
emitter.off('test', callback)
emitter.emit('test')
assertEqual(count, 1) // 移除后不再增加
})
})
})
}
3.3 完整示例:异步操作测试工具库
// AsyncTestHelper.ets - 异步测试工具库
export class AsyncTestHelper {
// 等待条件满足(轮询方式)
static async waitFor(
conditionFn: () => boolean,
options: { timeout?: number; interval?: number; message?: string } = {}
): Promise<void> {
const timeout = options.timeout ?? 5000
const interval = options.interval ?? 100
const message = options.message ?? '等待条件超时'
const startTime = Date.now()
while (!conditionFn()) {
if (Date.now() - startTime > timeout) {
throw new Error(message)
}
await new Promise(resolve => setTimeout(resolve, interval))
}
}
// 等待Promise并处理超时
static async withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
message: string = '操作超时'
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(message)), timeoutMs)
})
return Promise.race([promise, timeoutPromise])
}
// 延迟执行
static async delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
// 重试异步操作
static async retry<T>(
fn: () => Promise<T>,
options: { maxRetries?: number; delay?: number; shouldRetry?: (error: Error) => boolean } = {}
): Promise<T> {
const maxRetries = options.maxRetries ?? 3
const retryDelay = options.delay ?? 1000
const shouldRetry = options.shouldRetry ?? (() => true)
let lastError: Error | null = null
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (e) {
lastError = e as Error
if (attempt < maxRetries && shouldRetry(lastError)) {
await AsyncTestHelper.delay(retryDelay)
} else {
throw lastError
}
}
}
throw lastError!
}
// 并发执行多个异步操作并收集结果
static async concurrentAll<T>(
tasks: Array<() => Promise<T>>,
options: { concurrency?: number } = {}
): Promise<Array<{ success: boolean; result?: T; error?: Error }>> {
const concurrency = options.concurrency ?? tasks.length
const results: Array<{ success: boolean; result?: T; error?: Error }> = []
const executing: Promise<void>[] = []
for (let i = 0; i < tasks.length; i++) {
const task = tasks[i]
const promise = task()
.then(result => {
results[i] = { success: true, result }
})
.catch(error => {
results[i] = { success: false, error }
})
executing.push(promise)
if (executing.length >= concurrency) {
await Promise.race(executing)
// 移除已完成的
for (let j = executing.length - 1; j >= 0; j--) {
// 简化处理:等待所有完成
}
}
}
await Promise.all(executing)
return results
}
// 断言Promise被reject
static async expectReject(
promiseFn: () => Promise<unknown>,
expectedMessage?: string
): Promise<void> {
try {
await promiseFn()
throw new Error('期望Promise被reject,但它resolve了')
} catch (e) {
if ((e as Error).message === '期望Promise被reject,但它resolve了') {
throw e // 重新抛出我们的断言错误
}
if (expectedMessage) {
assertEqual((e as Error).message, expectedMessage)
}
}
}
// 断言Promise在指定时间内resolve
static async expectResolveWithin(
promiseFn: () => Promise<unknown>,
timeoutMs: number
): Promise<void> {
const start = Date.now()
await promiseFn()
const duration = Date.now() - start
assertTrue(duration <= timeoutMs, `操作耗时${duration}ms,超过${timeoutMs}ms限制`)
}
}
// AsyncTestHelperTest.ets - 异步测试工具的测试
import { describe, it, beforeEach } from '@ohos/hypium'
import { AsyncTestHelper } from '../../../main/ets/test/AsyncTestHelper'
export default function asyncTestHelperTest() {
describe('AsyncTestHelper异步测试工具', () => {
describe('waitFor等待条件', () => {
it('waitFor_条件满足_正常返回', 0, async () => {
let value = 0
setTimeout(() => { value = 42 }, 100)
await AsyncTestHelper.waitFor(() => value === 42, { timeout: 2000 })
assertEqual(value, 42)
})
it('waitFor_条件不满足_超时抛异常', 0, async () => {
let value = 0 // 永远不会变成42
try {
await AsyncTestHelper.waitFor(() => value === 42, {
timeout: 500,
interval: 100,
message: '值未变为42'
})
assertTrue(false, '应该抛出超时异常')
} catch (e) {
assertEqual((e as Error).message, '值未变为42')
}
})
})
describe('withTimeout超时控制', () => {
it('withTimeout_在超时前完成_返回结果', 0, async () => {
const fastPromise = new Promise<string>(resolve => {
setTimeout(() => resolve('完成'), 100)
})
const result = await AsyncTestHelper.withTimeout(fastPromise, 2000)
assertEqual(result, '完成')
})
it('withTimeout_超时_抛出异常', 0, async () => {
const slowPromise = new Promise<string>(resolve => {
setTimeout(() => resolve('太慢了'), 10000)
})
try {
await AsyncTestHelper.withTimeout(slowPromise, 500, '操作超时了')
assertTrue(false, '应该抛出超时异常')
} catch (e) {
assertEqual((e as Error).message, '操作超时了')
}
})
})
describe('retry重试机制', () => {
it('retry_首次成功_不重试', 0, async () => {
let attempts = 0
const result = await AsyncTestHelper.retry(async () => {
attempts++
return '成功'
})
assertEqual(result, '成功')
assertEqual(attempts, 1)
})
it('retry_前两次失败第三次成功_返回结果', 0, async () => {
let attempts = 0
const result = await AsyncTestHelper.retry(async () => {
attempts++
if (attempts < 3) throw new Error('还没好')
return '终于成功'
}, { maxRetries: 3, delay: 50 })
assertEqual(result, '终于成功')
assertEqual(attempts, 3)
})
it('retry_超过最大重试次数_抛出异常', 0, async () => {
try {
await AsyncTestHelper.retry(async () => {
throw new Error('永远失败')
}, { maxRetries: 2, delay: 50 })
assertTrue(false, '应该抛出异常')
} catch (e) {
assertEqual((e as Error).message, '永远失败')
}
})
})
describe('expectReject断言reject', () => {
it('expectReject_Promise被reject_通过', 0, async () => {
await AsyncTestHelper.expectReject(async () => {
throw new Error('预期的错误')
}, '预期的错误')
})
it('expectReject_Promise被resolve_抛出断言错误', 0, async () => {
try {
await AsyncTestHelper.expectReject(async () => {
return '不应该成功'
})
assertTrue(false, '应该抛出断言错误')
} catch (e) {
assertContain((e as Error).message, '期望Promise被reject')
}
})
})
describe('delay延迟', () => {
it('delay_延迟指定时间', 0, async () => {
const start = Date.now()
await AsyncTestHelper.delay(200)
const duration = Date.now() - start
assertTrue(duration >= 180, `延迟时间${duration}ms,不足200ms`)
assertTrue(duration < 500, `延迟时间${duration}ms,超过500ms`)
})
})
})
}
四、踩坑与注意事项
坑点1:忘记写async
it('test', 0, () => { await someAsync() })——测试函数没加async,await会报语法错误。更隐蔽的是:加了async但忘记await,异步操作"启动"了但测试已经结束,断言根本没执行。异步测试函数必须同时有async和await,缺一不可。
坑点2:done回调里忘了调用done
it('test', 0, (done) => { emitter.on('event', () => { assertEqual(1, 1) }) })——断言写了,但done()没调用。框架会一直等,直到超时。每个done回调模式的测试,必须在所有可能的执行路径上都调用done(),包括异常路径。
坑点3:done回调里抛异常
done模式下,如果回调函数里抛了异常,done不会被调用,测试超时。但错误信息只显示"超时",不显示真正的异常,排查非常困难。在done回调里用try-catch包裹,catch中调用done(error)。
坑点4:Promise.reject没被捕获
await service.riskyOperation(true)——如果这个Promise被reject了但没被try-catch包裹,未捕获的rejection会导致测试失败,但错误信息可能不太直观。所有可能reject的Promise都要用try-catch或.catch()处理。
坑点5:异步测试的超时时间不够
默认超时5秒,但有些操作确实需要更长时间(如大文件上传、慢网络请求)。如果测试经常因为超时失败,先确认是操作真的慢还是代码有Bug。操作确实慢的话,在测试配置中增加超时时间,别盲目加大。
坑点6:多个异步操作的执行顺序
await op1(); await op2()是串行的,Promise.all([op1(), op2()])是并行的。搞混了会导致测试结果不符合预期。特别是当op2依赖op1的结果时,用并行就会出问题。想清楚你的异步操作是串行还是并行,用对应的写法。
坑点7:async/await和done别混用
同一个测试函数里既用async/await又接收done参数,框架行为可能不符合预期——async函数返回的Promise和done回调是两套完成通知机制,混在一起容易出问题。选一种模式用到底,别混搭。
五、HarmonyOS 6适配说明
API差异表
| 功能/接口 | HarmonyOS 5 | HarmonyOS 6 | 变更说明 |
|---|---|---|---|
| 异步测试 | async/await + done | 同左 + @Timeout | 装饰器级超时控制 |
| 超时配置 | 全局配置 | @Timeout装饰器 | 用例级精细控制 |
| 异步断言 | 手动try-catch | assertResolves/assertRejects | 专用异步断言 |
| 异步钩子 | beforeAll支持async | 同左 + 超时隔离 | 钩子超时不影响用例 |
| 并发测试 | 无 | @Concurrent装饰器 | 异步用例并发执行 |
行为变更
-
@Timeout装饰器:HarmonyOS 6新增
@Timeout(ms)装饰器,可以为单个测试用例设置超时时间,不再需要全局配置或手动实现超时逻辑。 -
assertResolves/assertRejects:新增专用异步断言,
assertResolves(promise, expectedValue)验证Promise resolve的值,assertRejects(promise, ErrorType, message)验证Promise reject的异常。 -
钩子超时隔离:
beforeAll等钩子的超时不再影响后续用例的执行。钩子超时后会标记为错误,但用例仍会尝试执行(使用默认值或跳过)。
适配代码
// HarmonyOS 6异步测试新特性
import { describe, it, Timeout, assertResolves, assertRejects } from '@ohos/hypium'
describe('HarmonyOS 6异步测试', () => {
@Timeout(10000) // 给这个用例10秒超时
it('slowOperation_10秒内完成', 0, async () => {
const result = await slowService.fetch()
assertEqual(result.success, true)
})
it('assertResolves_Promise成功resolve', 0, async () => {
const promise = Promise.resolve(42)
await assertResolves(promise, 42)
})
it('assertRejects_Promise被reject', 0, async () => {
const promise = Promise.reject(new Error('出错了'))
await assertRejects(promise, Error, '出错了')
})
@Timeout(500) // 快速超时测试
it('timeout_500毫秒超时', 0, async () => {
// 如果操作超过500ms,测试自动标记为超时失败
await quickOperation()
})
})
六、总结
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ |
| 使用频率 | ⭐⭐⭐⭐⭐ |
| 重要程度 | ⭐⭐⭐⭐⭐ |
异步测试的核心就一句话:确保断言在异步操作完成后才执行。async/await是首选方案,代码清晰、错误自动传播;done回调是备选方案,适合回调风格的代码。超时处理不是可选项,是必选项——没有超时的异步测试,就像没有刹车的车,迟早出事。记住:异步测试的稳定性,取决于你对时序的控制力。
- 点赞
- 收藏
- 关注作者
评论(0)