HarmonyOS开发:异步测试与Promise测试

举报
Jack20 发表于 2026/06/24 15:14:09 2026/06/24
【摘要】 HarmonyOS开发:异步测试与Promise测试📌 核心要点:异步代码的测试不是"加个await就完事了"——你得搞清楚Promise什么时候resolve、超时怎么处理、并发异步怎么控制。Hypium提供了async/await和done回调两种模式,选对了模式,异步测试才能稳。 一、背景与动机你的业务代码里有异步操作吗?网络请求、文件读写、定时器、数据库查询——哪个不是异步的?但...

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怎么知道你的异步操作完成了?三种方式告诉它:

  1. async/await:用async标记测试函数,await等待Promise resolve,框架自动处理
  2. done回调:测试函数接收一个done参数,你手动调用done()表示完成
  3. 返回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() })——测试函数没加asyncawait会报语法错误。更隐蔽的是:加了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装饰器 异步用例并发执行

行为变更

  1. @Timeout装饰器:HarmonyOS 6新增@Timeout(ms)装饰器,可以为单个测试用例设置超时时间,不再需要全局配置或手动实现超时逻辑。

  2. assertResolves/assertRejects:新增专用异步断言,assertResolves(promise, expectedValue)验证Promise resolve的值,assertRejects(promise, ErrorType, message)验证Promise reject的异常。

  3. 钩子超时隔离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回调是备选方案,适合回调风格的代码。超时处理不是可选项,是必选项——没有超时的异步测试,就像没有刹车的车,迟早出事。记住:异步测试的稳定性,取决于你对时序的控制力

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。