系列质量保障篇·第37篇。AI篇后,有资深开发者提问:“Demo跑通了,AI也接上了,但怎么保证每次修改代码后,之前的功能不被破坏?尤其是分布式流转和AI推理,怎么自动化测试?” 这触及了工程化开发的核心——测试。很多个人开发者忽视测试,导致项目后期难以维护。今天我们将电商Demo纳入测试驱动开发(TDD)轨道,使用Hypium框架编写单元测试、UI测试、分布式场景测试,并集成DevEco Testing实现自动化。全程基于API23,含官方文档未涉及的“分布式Mock技巧”和“AI模型确定性测试”方案。

一、前言:为什么测试是“高级开发者的分水岭”?

在单人开发Demo阶段,手动点几下看看效果似乎够用。但在团队协作、商业交付场景下,手动测试的弊端暴露无遗:

  1. 回归测试噩梦:改了一个Bug,却引入了三个新Bug。每次发版前,测试人员需要把所有功能重测一遍,耗时耗力。

  2. 分布式场景难复现:“手机流转到手表后,数据偶尔丢失”——这种偶发问题很难通过手动操作抓到。

  3. AI结果的不确定性:AI模型的输出有随机性,如何确保每次更新模型后,抠图效果没有变差?

解决方案:测试驱动开发(TDD)。核心思想是“先写测试,后写代码”。测试不仅是验证工具,更是活文档设计工具。它能强制你写出低耦合、高内聚的代码。

HarmonyOS提供了强大的Hypium测试框架,支持从底层逻辑到上层UI,再到跨设备分布式场景的全链路测试。

二、核心概念辨析(测试金字塔)

测试类型

层级

速度

成本

覆盖范围

适用场景

单元测试

底层

极快

单个函数/类

验证业务逻辑、算法正确性

UI测试

中层

单个页面/组件

验证界面交互、组件渲染

分布式场景测试

顶层

多设备协作流程

验证流转、同步、数据一致性

手动测试

顶层

极慢

极高

整体用户体验

探索性测试、主观体验评估

最佳实践:遵循“测试金字塔”模型,大量单元测试,适量UI测试,少量分布式场景测试。

三、代码实现:从“裸奔”到“测试全覆盖”

3.1 环境准备与目录结构

在DevEco Studio中,测试代码位于entry_test目录下。



entry/
├── src/
│   ├── main/          # 主代码
│   └── test/           # 测试代码
│       ├── js/
│       │   ├── unit/  # 单元测试
│       │   ├── ui/    # UI测试
│       │   └── dist/  # 分布式场景测试
│       └── resources/ # 测试资源

3.2 单元测试:验证核心逻辑

以之前的CartCalculator为例,编写单元测试。

创建entry_test/src/test/js/unit/CartCalculator.test.ets



import { describe, it, expect, beforeAll, afterAll } from '@ohos/hypium'
import { CartCalculator } from '../../../../src/main/ets/model/CartCalculator'

export default function cartCalculatorTest() {
  describe('CartCalculator单元测试', () => {
    let calculator: CartCalculator

    beforeAll(() => {
      calculator = new CartCalculator()
    })

    afterAll(() => {
      // 清理工作
    })

    // 测试正常计算总价
    it('should_calculate_total_price_correctly', 0, () => {
      const items = [
        { price: 100, count: 2 },
        { price: 50, count: 1 }
      ]
      const total = calculator.calculateTotal(items)
      expect(total).assertEqual(250)
    })

    // 测试空购物车
    it('should_return_zero_for_empty_cart', 0, () => {
      const total = calculator.calculateTotal([])
      expect(total).assertEqual(0)
    })

    // 测试边界值:数量为0
    it('should_handle_zero_quantity', 0, () => {
      const items = [{ price: 100, count: 0 }]
      const total = calculator.calculateTotal(items)
      expect(total).assertEqual(0)
    })

    // 测试优惠逻辑:满300减50
    it('should_apply_discount_when_total_over_300', 0, () => {
      const items = [{ price: 200, count: 2 }] // 400元
      const total = calculator.calculateTotal(items)
      expect(total).assertEqual(350) // 400 - 50
    })
  })
}

3.3 UI测试:验证界面交互

UI测试模拟用户操作,验证界面是否正确响应。

创建entry_test/src/test/js/ui/ProductPage.test.ets



import { describe, it, expect, UiDriver, By } from '@ohos/hypium'
import { Driver } from '@kit.TestKit'

export default function productPageTest() {
  describe('商品页面UI测试', () => {
    let driver: UiDriver

    beforeAll(async () => {
      driver = await UiDriver.create()
      // 启动应用
      await driver.startAbility({
        bundleName: 'com.example.shop',
        abilityName: 'EntryAbility'
      })
      // 等待页面加载
      await driver.delayMs(2000)
    })

    afterAll(async () => {
      await driver.stopAbility()
    })

    // 测试点击加购按钮
    it('should_add_to_cart_when_click_add_button', 0, async () => {
      // 1. 查找“加入购物车”按钮
      const addButton = await driver.findComponent(By.text('加入购物车'))
      expect(addButton).assertNotNull()

      // 2. 点击按钮
      await addButton.click()
      await driver.delayMs(500)

      // 3. 验证购物车数量是否更新
      const cartBadge = await driver.findComponent(By.id('cart_badge'))
      const text = await cartBadge.getText()
      expect(text).assertEqual('1')

      // 4. 验证Toast提示
      const toast = await driver.findComponent(By.textContains('已加入购物车'))
      expect(toast).assertNotNull()
    })

    // 测试滑动浏览商品
    it('should_scroll_product_list_smoothly', 0, async () => {
      const list = await driver.findComponent(By.id('product_list'))
      expect(list).assertNotNull()

      // 向上滑动
      await list.swipeUp()
      await driver.delayMs(300)

      // 向下滑动
      await list.swipeDown()
      await driver.delayMs(300)

      // 验证滑动后没有崩溃
      const title = await driver.findComponent(By.text('HarmonyOS手机'))
      expect(title).assertNotNull()
    })
  })
}

3.4 分布式场景测试:Mock与多设备协同

这是鸿蒙测试的难点。真实测试需要多台物理设备,成本高且不稳定。解决方案是Mock(模拟)分布式环境

创建entry_test/src/test/js/dist/DistributedFlow.test.ets



import { describe, it, expect, mock, afterEach } from '@ohos/hypium'
import { distributedDataObject } from '@kit.ArkData'
import { DistributedManager } from '../../../../src/main/ets/manager/DistributedManager'

// Mock分布式数据对象
mock(distributedDataObject, 'createDistributedDataObject', (context: Context, config: any) => {
  console.log('Mock: createDistributedDataObject called')
  return {
    on: (event: string, callback: Function) => {
      console.log(`Mock: Registered listener for ${event}`)
      // 模拟数据同步事件
      if (event === 'dataChanged') {
        setTimeout(() => {
          callback({ 'latestOrder': { orderId: 'mock_order_123' } })
        }, 100)
      }
    },
    off: (event: string) => {
      console.log(`Mock: Unregistered listener for ${event}`)
    },
    sync: async (devices: string[]) => {
      console.log(`Mock: Syncing data to devices: ${devices}`)
      return true
    }
  }
})

export default function distributedFlowTest() {
  describe('分布式流转场景测试', () => {
    let manager: DistributedManager

    beforeEach(() => {
      manager = new DistributedManager()
    })

    afterEach(() => {
      mock.reset()
    })

    // 测试手机到手表的流转
    it('should_sync_order_data_from_phone_to_watch', 0, async () => {
      // 1. 初始化分布式管理器
      await manager.init(globalThis.context)

      // 2. 模拟手机端创建订单
      const order = { orderId: 'test_001', amount: 5999 }
      await manager.createOrder(order)

      // 3. 验证数据同步是否触发
      // 由于我们使用了Mock,这里验证的是逻辑流程,而非真实设备
      const syncedData = await manager.getSyncedData()
      expect(syncedData).assertNotNull()
      expect(syncedData.orderId).assertEqual('mock_order_123')

      console.log('分布式流转逻辑验证成功')
    })

    // 测试网络断开后的重连机制
    it('should_resync_when_network_recovers', 0, async () => {
      await manager.init(globalThis.context)

      // 模拟网络断开
      manager.simulateNetworkDisconnect()
      expect(manager.isSyncing()).assertFalse()

      // 模拟网络恢复
      await manager.simulateNetworkRecover()
      expect(manager.isSyncing()).assertTrue()

      // 验证数据是否重新同步
      const syncedData = await manager.getSyncedData()
      expect(syncedData).assertNotNull()
    })
  })
}

3.5 AI模型确定性测试

AI模型的输出有随机性,但测试需要确定性。解决方案是固定随机种子结果比对

创建entry_test/src/test/js/unit/AIModel.test.ets



import { describe, it, expect, mock } from '@ohos/hypium'
import { ImageSegmentation } from '../../../../src/main/ets/ai/ImageSegmentation'
import { image } from '@kit.ImageKit'

export default function aiModelTest() {
  describe('AI模型确定性测试', () => {
    let segmentation: ImageSegmentation

    beforeAll(async () => {
      segmentation = new ImageSegmentation()
      await segmentation.init(globalThis.context)
    })

    // 测试相同输入产生相同输出(确定性)
    it('should_produce_deterministic_output_for_same_input', 0, async () => {
      // 1. 加载固定的测试图片
      const testImage = await loadFixedTestImage()
      
      // 2. 第一次推理
      const result1 = await segmentation.segmentProduct(testImage)
      const hash1 = await calculateImageHash(result1)

      // 3. 第二次推理(相同输入)
      const result2 = await segmentation.segmentProduct(testImage)
      const hash2 = await calculateImageHash(result2)

      // 4. 验证两次结果一致
      expect(hash1).assertEqual(hash2)
      console.log('AI模型输出具有确定性')
    })

    // 测试模型降级逻辑
    it('should_fallback_to_cpu_when_npu_unavailable', 0, async () => {
      // Mock NPU不可用
      mock(segmentation, 'initNPU', async () => {
        throw new Error('NPU not available')
      })

      // 重新初始化,应触发降级
      await segmentation.init(globalThis.context)

      // 验证是否使用了CPU后端
      const backend = segmentation.getCurrentBackend()
      expect(backend).assertEqual('CPU')
      console.log('AI模型成功降级为CPU推理')
    })
  })
}

// 辅助函数:加载固定测试图片
async function loadFixedTestImage(): Promise<image.PixelMap> {
  // ... 加载一张固定的、已知的图片 ...
  return {} as image.PixelMap
}

// 辅助函数:计算图片哈希值(用于比较相似度)
async function calculateImageHash(pixelMap: image.PixelMap): Promise<string> {
  // ... 实现简单的图片哈希算法 ...
  return ''
}

四、踩坑记录(官方文档没写的测试细节)

  1. 异步测试陷阱:Hypium的it函数默认是同步的。如果你的测试用例包含异步操作(如await),必须在it的第二个参数中传入0,并将测试函数声明为async。否则测试会提前结束,断言不会执行。

    
      
    
      
    // 正确
    it('async_test', 0, async () => { ... })
    // 错误
    it('async_test', () => { ... })
  2. UI测试的时序问题:UI渲染和动画是异步的。在点击按钮后立即断言,可能因为UI还没更新而失败。必须使用driver.delayMs(500)或等待特定组件出现。

  3. Mock的局限性:Mock能模拟分布式环境,但无法模拟真实的网络延迟和设备性能差异。分布式测试必须包含真机测试,Mock只能作为辅助。

  4. 测试资源的清理:每个测试用例(it)结束后,必须清理测试数据(如删除临时文件、重置全局状态)。否则,前一个测试的状态可能影响后一个测试,导致“间歇性失败”。

  5. AI测试的随机性:即使固定了随机种子,不同硬件(CPU指令集差异)也可能导致浮点数计算结果有微小差异。在比较AI结果时,应使用误差容忍(如expect(diff).assertLess(0.001)),而非精确相等。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐