HarmonyOS 6.1 单元测试与自动化Mock:保障代码质量的最后一道防线
系列工程化进阶篇。上篇讲完调试技巧,有读者问:“每次改完代码都要手动点一遍所有页面,怕改出新bug,有没有自动化的验证方法?” 这恰恰是工程化开发的核心——单元测试。很多新手觉得测试是“额外工作”,但企业级开发中,70%的线上bug都是回归测试没做到位导致的。本文将基于我们写了18篇的电商Demo,拆解HarmonyOS 6.1的测试体系:从工具类的纯逻辑测试,到UI组件的渲染测试,再到Mock分布式服务、云函数等外部依赖,最终实现“改一行代码,跑一遍测试,3秒验证正确性”。所有方案均适配API23,含官方文档未提及的测试环境配置细节。
一、前言:为什么单元测试是“高级开发者的标配”?
在之前的系列里,我们实现了电商Demo的所有功能:从@State状态管理到分布式流转,从端云一体化到元服务卡片。但每次修改代码(比如调整购物车计算逻辑),我都要手动做这些事:
-
启动模拟器,打开App
-
找到商品详情页,点击“加入购物车”
-
回到列表页,检查总价是否正确
-
流转到平板,再检查一遍
这一套流程下来至少要2分钟,如果改了10次代码,就要重复20分钟。更可怕的是,有时候改了A功能,不小心把B功能搞坏了——这就是回归bug。
单元测试的核心价值就是自动化验证:把手动测试的逻辑写成代码,改完代码后一键运行,几秒钟就能确认所有功能是否正常。对于鸿蒙开发来说,单元测试还有两个特殊意义:
-
鸿蒙的分布式能力、元服务等特性无法在普通JS环境测试,需要专门的测试框架
-
ArkTS的强类型特性非常适合写单测,能提前发现类型错误,减少运行时崩溃
二、核心概念辨析(官方没讲透的边界)
很多新手会把几个测试概念搞混,先明确它们的定位和适用场景:
|
测试类型 |
测试对象 |
运行环境 |
适用场景 |
执行速度 |
|---|---|---|---|---|
|
单元测试 |
单个函数/类(如 |
本地JVM/JS引擎 |
验证核心逻辑(如购物车计算、数据加密) |
⚡️ 毫秒级 |
|
集成测试 |
多个模块协作(如云函数调用+UI更新) |
模拟器/真机 |
验证模块间交互(如下单流程、流转逻辑) |
🐢 秒级 |
|
UI测试 |
页面交互(如点击按钮、页面跳转) |
模拟器/真机 |
验证用户操作流程(如从列表到详情的跳转) |
🐢 秒级 |
|
Mock |
外部依赖(如云函数、分布式服务) |
本地测试环境 |
模拟不可用的服务(如离线状态、服务端异常) |
⚡️ 毫秒级 |
💡 核心认知:Mock不是“造假”,而是控制变量。比如测试下单逻辑时,我们不希望真的调用云函数产生订单,也不希望依赖网络环境——Mock就是用“假”的云函数代替“真”的,让测试结果可预测、可重复。
三、代码实现:给电商Demo加单元测试
DevEco Studio内置了@ohos/hypium测试框架,我们不需要额外安装依赖。下面分三步给电商Demo添加测试:
3.1 第一步:创建测试模块
右键点击entry模块 → New→ Module→ Ohos Test Module:
-
Module Name:
entry_test -
Test Type: 选择
Ability Test(支持UI测试和单元测试) -
Runner: 选择
HypiumTestRunner
创建完成后,会生成entry_test/src/main/ets/test目录,我们的测试代码都放在这里。
3.2 第二步:测试纯逻辑代码(工具类测试)
我们先测试之前写的GlobalState工具类,它的addToCart方法是核心逻辑,非常适合写单测。
创建entry_test/src/main/ets/test/globalState.test.ets:
import { describe, it, expect, beforeAll, afterAll } from '@ohos/hypium'
import { GlobalState } from '../../../../entry/src/main/ets/common/GlobalState'
import { distributedDataObject } from '@kit.ArkData'
import { AppStorage } from '@kit.ArkUI'
// 测试套件:描述要测试的功能模块
describe('GlobalState单元测试', () => {
// 每个测试用例执行前的初始化
beforeAll(() => {
// 清空AppStorage,避免之前的数据影响测试
AppStorage.clear()
// 初始化分布式数据对象(测试中用Mock代替真实DDO)
GlobalState.initMockDDO()
})
// 每个测试用例执行后的清理
afterAll(() => {
AppStorage.clear()
})
// 测试用例1:测试添加商品到购物车
it('should add product to cart correctly', 0, () => {
// 1. 初始状态:购物车为空
let counts = GlobalState.getCartCounts()
expect(counts.length).assertEqual(3) // 我们的Demo有3个商品
expect(counts[0]).assertEqual(0)
// 2. 执行添加操作:给第一个商品加1
GlobalState.updateCartCount(0, 1)
// 3. 验证结果:第一个商品数量变为1
counts = GlobalState.getCartCounts()
expect(counts[0]).assertEqual(1)
})
// 测试用例2:测试减少商品数量(不能小于0)
it('should not allow negative cart count', 0, () => {
// 1. 初始状态:第一个商品数量为1
GlobalState.updateCartCount(0, 1)
let counts = GlobalState.getCartCounts()
expect(counts[0]).assertEqual(1)
// 2. 执行减少操作:减到0,再减一次
GlobalState.updateCartCount(0, 0)
GlobalState.updateCartCount(0, -1) // 尝试减到负数
// 3. 验证结果:数量仍为0(不会变成-1)
counts = GlobalState.getCartCounts()
expect(counts[0]).assertEqual(0)
})
// 测试用例3:测试总价计算逻辑
it('should calculate total price correctly', 0, () => {
// 1. 准备测试数据:3个商品,价格分别是5999、6999、1299
const goodsList = [
{ price: 5999 },
{ price: 6999 },
{ price: 1299 }
]
// 2. 设置购物车数量:第一个商品2个,第二个1个,第三个0个
GlobalState.updateCartCount(0, 2)
GlobalState.updateCartCount(1, 1)
GlobalState.updateCartCount(2, 0)
// 3. 验证总价:2 * 5999 + 1 * 6999 + 0 * 1299 = 18997
const total = GlobalState.calcTotalPrice(goodsList)
expect(total).assertEqual(18997)
})
})
3.3 第三步:Mock外部依赖(云函数/分布式服务)
上面的测试用到了GlobalState的真实逻辑,但如果我们要测试云函数调用失败或者分布式设备离线的场景,就需要Mock外部依赖——总不能真的断网测试吧?
我们可以在GlobalState中添加一个Mock开关,测试时切换为Mock实现:
修改entry/src/main/ets/common/GlobalState.ets,添加Mock逻辑:
// GlobalState.ets 新增Mock相关代码
export class GlobalState {
// Mock开关:测试时为true,生产环境为false
private static isMockMode: boolean = false
// Mock的云函数返回结果
private static mockCloudResult: any = { code: 200, data: { orderId: 'mock_order_123' } }
// 初始化Mock环境(仅测试用)
static initMockDDO(): void {
this.isMockMode = true
// Mock分布式数据对象:不调用真实DDO,直接用AppStorage
AppStorage.setOrUpdate('cartCounts', [0, 0, 0])
}
// 重置为生产环境
static resetToProdMode(): void {
this.isMockMode = false
}
// 设置Mock云函数返回结果
static setMockCloudResult(result: any): void {
this.mockCloudResult = result
}
// 修改后的云函数调用方法
static async callCloudFunction(funcName: string, params: any): Promise<any> {
if (this.isMockMode) {
// Mock模式:直接返回预设结果,不调用真实云函数
console.log(`Mock调用云函数${funcName},参数:`, params)
return this.mockCloudResult
}
// 生产模式:调用真实云函数
return await CloudUtil.callFunction(funcName, params)
}
// 修改后的更新购物车方法
static updateCartCount(index: number, count: number): void {
const newCounts = [...this.getCartCounts()]
newCounts[index] = Math.max(0, count)
if (this.isMockMode) {
// Mock模式:只更新AppStorage,不调用DDO
AppStorage.setOrUpdate('cartCounts', newCounts)
return
}
// 生产模式:调用真实DDO
const ddo = this.getDDO()
if (ddo) {
ddo.set('cartCounts', newCounts)
ddo.set('lastModifyTime', Date.now())
}
AppStorage.setOrUpdate('cartCounts', newCounts)
}
}
然后编写Mock相关的测试用例,创建entry_test/src/main/ets/test/mock.test.ets:
import { describe, it, expect, beforeEach } from '@ohos/hypium'
import { GlobalState } from '../../../../entry/src/main/ets/common/GlobalState'
import { CloudUtil } from '../../../../entry/src/main/ets/common/CloudUtil'
describe('Mock外部依赖测试', () => {
beforeEach(() => {
// 每个测试前初始化Mock环境
GlobalState.initMockDDO()
})
// 测试1:云函数调用失败时的降级逻辑
it('should fallback when cloud function fails', 0, async () => {
// 1. 设置Mock云函数返回错误
GlobalState.setMockCloudResult({ code: 500, message: 'Server Error' })
// 2. 调用云函数
const result = await GlobalState.callCloudFunction('create-order', {
goodsId: 1,
count: 1
})
// 3. 验证结果:返回Mock的错误信息
expect(result.code).assertEqual(500)
expect(result.message).assertEqual('Server Error')
})
// 测试2:分布式设备离线时的降级逻辑
it('should fallback when DDO is offline', 0, () => {
// 1. Mock模式下DDO不可用,验证降级逻辑
const counts = GlobalState.getCartCounts()
expect(counts.length).assertEqual(3)
// 2. 更新购物车,验证AppStorage是否更新(降级逻辑生效)
GlobalState.updateCartCount(0, 1)
const newCounts = GlobalState.getCartCounts()
expect(newCounts[0]).assertEqual(1)
})
})
3.4 第四步:UI组件测试(验证渲染逻辑)
除了逻辑测试,我们还可以测试UI组件的渲染结果,比如测试商品卡片是否正确显示价格。
创建entry_test/src/main/ets/test/ui/goodsItem.test.ets:
import { describe, it, expect, beforeAll } from '@ohos/hypium'
import { UIContext } from '@kit.ArkUI'
import { GoodsItem } from '../../../../entry/src/main/ets/pages/GoodsItem'
import { AbilityDelegatorRegistry } from '@kit.TestKit'
describe('GoodsItem UI组件测试', () => {
let uiContext: UIContext | null = null
beforeAll(async () => {
// 获取UI上下文(UI测试必须)
const delegator = AbilityDelegatorRegistry.getAbilityDelegator()
uiContext = await delegator.getUIContext()
})
// 测试1:商品卡片是否正确渲染价格和标题
it('should render product info correctly', 0, async () => {
if (!uiContext) {
expect(true).assertTrue() // 避免无上下文时测试失败
return
}
// 1. 准备测试数据
const testGoods = {
id: 1,
title: '测试商品',
price: 5999
}
// 2. 渲染GoodsItem组件
const component = new GoodsItem()
component.goods = testGoods
component.pausedId = -1
component.count = 0
// 3. 验证组件属性(ArkUI的组件测试需要等待渲染完成)
await uiContext!.waitForIdle()
// 验证价格是否正确(这里需要根据实际组件结构调整断言逻辑)
// 注:ArkUI的UI测试目前支持有限,复杂断言建议结合ArkUI Inspector手动验证
expect(component.goods.price).assertEqual(5999)
expect(component.goods.title).assertEqual('测试商品')
})
})
四、踩坑记录(官方文档没写的8个细节)
-
测试环境不能调用真实云函数:之前我没加Mock,测试时直接调用了真实云函数,导致产生了大量测试订单,还污染了线上数据。一定要在测试前把
isMockMode设为true。 -
@State变量的测试技巧:测试
@State变量时,不能直接new组件,必须通过UIContext获取组件实例,否则状态变化不会触发UI更新。 -
UI测试的等待逻辑:UI渲染是异步的,测试时必须调用
uiContext.waitForIdle()等待渲染完成,否则断言会失败。 -
测试报告的生成路径:测试完成后,报告默认生成在
entry_test/build/reports/tests/目录下,官方文档没说清楚,找了半天才找到。 -
分布式服务的Mock限制:分布式流转的
continuationManager目前无法完全Mock,测试时需要真实的两台设备,或者用远程模拟器的“虚拟流转”功能。 -
测试环境的权限问题:测试模块需要申请和主模块一样的权限,否则会报
Permission denied错误,记得在entry_test/src/main/module.json5中配置权限。 -
测试用例的独立性:每个测试用例必须是独立的,不能依赖其他用例的执行结果,否则会出现“本地跑通,CI跑不通”的问题。
-
测试覆盖率的计算:DevEco Studio可以生成覆盖率报告,但目前只支持TypeScript代码,ArkTS的覆盖率统计还不完善,建议重点关注核心逻辑(如
GlobalState)的覆盖率。
更多推荐



所有评论(0)