单元测试与 UI 测试:arkXtest 框架与工程实践
一、引言
短视频工程的复杂度横跨六个设备形态、四个入口 HAP 与若干特性模块,任何一次"顺手重构"都可能悄悄破坏断点判定、数据模型默认值或窗口状态同步这类基础能力。若没有自动化测试兜底,回归只能靠人肉点测——而多设备场景下,人肉回归的覆盖面天然不足。multi-short-video 工程目前尚未建立 test 目录,本文基于工程内真实的可测单元(WidthBreakpointType、MSVDataModel、CommonConstants、Logger等),讲解如何用 arkXtest 框架补齐单元测试与 UI 测试,覆盖从工具类到页面交互的质量闭环。
HarmonyOS 的测试体系分三层:arkXtest 单元测试(验证函数/类的行为)、UI 测试(驱动真机/模拟器验证界面交互)、性能与稳定性测试。本文聚焦前两者。
测试对多设备工程还有一层特殊意义:六个形态、四个入口意味着回归面被放大数倍,而人工回归往往只覆盖主路径。自动化用例充当"最低保障网"——它不替代人工测试,但保证每次改动后,断点判定、数据默认值、页签切换这些基础行为不会悄悄退化。本文所有示例都基于工程现有代码,可直接复制到新建设的测试目录中运行。
二、arkXtest 框架基础与测试目录结构
arkXtest 是 ArkTS 官方的单元测试框架,语法与 Jest 类似:describe组织测试套件、it声明单条用例、expect做断言。工程建议在模块下新建ohosTest目录(DevEco Studio 新建模块时默认生成),结构如下:
features/multishortvideobase(以公共模块为例) └── ohosTest/ └── ets/ └── test/ ├── List.test.ets # 工具类与模型测试 └── Ability.test.ets # 模块级冒烟测试TestRunner与测试套件注册在module.json5或测试配置中声明,IDE 与命令行均可触发。一个最小用例:
// ohosTest/ets/test/List.test.ets import { describe, it, expect } from '@ohos/hypium'; export default function listTest() { describe('CommonConstantsTest', () => { it('FULL_PERCENT_should_be_100_percent', 0, () => { expect(CommonConstants.FULL_PERCENT).assertEqual('100%'); }); }); }describe名称使用 PascalCase 表达被测对象,it的第二个参数是用例编号(同一 describe 内递增),断言统一走expect(...).assertXxx(...)链式 API。arkXtest 常用断言如下:
| 断言 | 说明 | 使用场景 |
assertEqual | 值相等 | 返回字符串、数字、枚举 |
assertTrue/assertFalse | 布尔成立 | 条件分支、存在性判断 |
assertNull/assertNotNull | 是否为空 | 可选字段、可选参数 |
assertLarger/assertLess | 数值比较 | 列表长度、计数 |
assertContain | 包含关系 | 字符串、数组成员 |
assertThrowError | 期望抛错 | 非法参数防御 |
数值与字符串断言尽量选择精确匹配,避免用布尔断言"糊"过去;对可能为 undefined 的字段用assertNull/assertNotNull明确表达预期。
三、针对工程真实代码编写测试用例
工程中最值得测、也最好测的单元,是"无 UI 依赖的纯逻辑"。以断点工具WidthBreakpointType(common/multishortvideobase/src/main/ets/utils/WidthBreakpointType.ets)为例,它接收四个断点取值并按当前宽度返回对应项,是评论区、个人页、TabBar 大量布局分支的决策核心,但当前没有一条测试用例——这正是单测的首选目标:
it('getValue_should_pick_value_by_breakpoint', 0, () => { const bp = new WidthBreakpointType<string>('xs', 'sm', 'md', 'lg'); expect(bp.getValue(WidthBreakpoint.WIDTH_XS)).assertEqual('xs'); expect(bp.getValue(WidthBreakpoint.WIDTH_SM)).assertEqual('sm'); expect(bp.getValue(WidthBreakpoint.WIDTH_MD)).assertEqual('md'); expect(bp.getValue(WidthBreakpoint.WIDTH_LG)).assertEqual('lg'); }); it('getValue_should_fallback_to_last_when_out_of_range', 1, () => { const bp = new WidthBreakpointType<number>(10, 20, 30, 40); expect(bp.getValue(WidthBreakpoint.WIDTH_XL)).assertEqual(40); });数据模型同样适合单测。MSVDataModel(common/multishortvideobase/src/main/ets/model/MSVDataModel.ets)的构造函数带默认值逻辑,可以锁定"未传参时的兜底行为":
it('iconSize_should_default_to_36', 0, () => { const model = new MSVDataModel(home, $r('app.string.home_title')); expect(model.iconOnly).assertFalse(); expect(model.iconSize).assertEqual(36); });除了公共模块,特性模块的纯逻辑同样值得覆盖。以评论数据模型CommentDataModel(features/multishortvideocomment/.../model/CommentDataModel.ets)与视频数据模型AvDataSourceModel(features/multishortvideoadaptivevideo/.../model/AvDataSourceModel.ets)为例,它们承载了评论内容、回复列表、视频比例与播放地址等字段,直接决定 UI 渲染内容。测试可断言:构造参数能正确落位(如currentSource与 rawfile 路径一致)、可选字段(如replys)为空时的默认行为、以及AvDataSourceViewModel返回的列表与资源定义的数量一致。这类用例把"数据模型 → ViewModel → UI"链路的前两环锁住,UI 层出问题时能迅速排除数据层嫌疑。
再如Logger工具类(common/.../utils/Logger.ets),虽然它的输出目标是 hilog,但可以通过注入方式验证其不抛异常:把 hilog 调用封装在可替换的接口后面,测试中注入桩实现,断言"各级别方法在正常/异常参数下都能安全调用"。这类测试的价值不在断言本身,而在于防止后续重构破坏公共 API 的签名与默认行为——MSVDataModel构造参数顺序、WidthBreakpointType的取值顺序一旦变动,测试会第一时间报警。
class LoggerStub { public lastArgs: Object[] = []; public info(...args: Object[]): void { this.lastArgs = args; } } it('logger_should_not_throw_when_empty_args', 0, () => { const stub = new LoggerStub(); const logger = new Logger('test'); expect(() => logger.info('hello', 'world')).not.assertThrowError(); });依赖注入的边界设计还有一层工程考量:被测对象应"接近真实"而非"过度打桩"。ViewModel 的 mock 数据直接来自 string.json 资源与本地文件,测试断言的是"真实资源经过 ViewModel 加工后的结果",比纯手工构造的假数据更有回归价值;只有当外部能力(hilog、窗口、数据库)会带来不确定性时,才用桩替换。这条原则避免了测试"测了个寂寞"——用例验证的必须是生产路径上真实发生的逻辑,而不是一套只在测试环境存在的平行实现。
四、UI 测试与组件定位
UI 测试基于@ohos.UiTest(即 UiTest 框架)驱动真实页面:先Driver.create()建立会话,再用By查找器按 id、text、类型定位组件,最后触发点击/滑动并断言结果。工程组件普遍设置了id(如MSVTabs内部MSVTabContent的stackId),为 UI 测试提供了稳定锚点:
import { Driver, ON, By } from '@ohos.UiTest'; export default function uiTest() { describe('IndexUITest', () => { it('tabBar_should_switch_to_mine_tab', 0, async () => { const driver = Driver.create(); // 按文本定位"我的"页签并点击 const mineTab = await driver.findComponent(By.text('我的')); await mineTab.click(); // 断言个人页关键文案出现 const intro = await driver.findComponent(By.text('点击添加介绍,让大家认识你...')); expect(intro).assertTrue(); }); }); }UI 测试要点:
- 优先按 id 定位:文案随语言切换,
By.text会因语言环境失效,组件id与语言无关,是更稳的锚点; - 等待与超时:异步渲染、动画转场需要
await driver.waitForComponent(...)或合理超时,避免时序抖动; - 多设备矩阵:工程六个形态差异大,UI 用例需标注运行设备(
deviceType),至少覆盖直板机与平板两种断点,才能覆盖"分栏/全窗口"两类布局分支; - 隔离外部依赖:测试环境禁用真实网络与系统级交互,评论、作品等数据全部走 ViewModel 内置的 mock 数据源(见下节),保证用例可重复。
五、Mock 数据与依赖注入
工程的 ViewModel 层天然适合"依赖注入 + Mock":CommentViewModel().getCommentList()、MainTabsViewModel().getMainTabsData()、AvDataSourceViewModel().getAvDataSource()都返回内存中的 mock 数据(数据取自 string.json 资源与本地 rawfile 视频),没有网络与数据库依赖。这让单元测试几乎不需要额外 mock 框架:
it('getCommentList_should_return_non_empty', 0, () => { const vm = new CommentViewModel(); const list = vm.getCommentList(); expect(list.length).assertLarger(0); expect(list[0].name).assertTrue(); });当被测对象需要注入外部能力时(如Logger的 hilog、WindowUtil的窗口对象),采用构造器注入或可替换的模块级引用:测试中替换为桩实现,验证调用关系与参数,而不是真实能力。这与本文第五节(日志)中"把 hilog 封装在可替换接口后"的思路一致,接口的稳定边界同时服务了可测性。
六、测试脚本执行与覆盖率
测试在 DevEco Studio 中可直接右键用例运行;命令行场景由 hvigor 驱动,常用命令:
# 在模块目录下执行 ohosTest 测试 hvigorw --mode module -p module=multishortvideobase@default -p isTest=true assembleHap hvigorw test --buildMode debug覆盖率方面,DevEco Studio 的测试面板提供行覆盖率与分支覆盖率统计。工程实践建议按优先级推进:第一阶段覆盖纯逻辑层(断点工具、数据模型、ViewModel 的 mock 数据方法),目标行覆盖率 80% 以上;第二阶段补充关键路径的 UI 用例(首页页签切换、评论半模态打开、个人页九宫格切换),不追求数量,追求"每个断点分支都有用例经过"。覆盖率低的分支(如多设备差异分支)恰好就是多设备回归的高风险区,值得优先补测。
CI 集成是测试发挥价值的最后一公里。工程建议在推送流水线中挂两条测试任务:unit test(跑全部 ohosTest 单测,失败即阻断合入)与ui test(真机矩阵按设备形态抽样,至少覆盖直板机与平板)。覆盖率报告作为制品归档,每个迭代对比增量代码的覆盖缺口,形成"新增逻辑必带用例"的团队约定。测试耗时控制上,单测应控制在分钟级以内——若某个用例超过秒级,通常意味着它依赖了不该依赖的外部能力,需要继续拆分 Mock 边界。
最后回到工程整体视角,测试应当按金字塔结构布局:底层是大量廉价的单元测试(断点工具、数据模型、ViewModel),中层是少量 UI 组件测试(页签切换、半模态、九宫格),顶层是极少数端到端冒烟(多设备矩阵)。金字塔越往上,维护成本与执行时间越高,用例数量应当越少。工程目前的起步建议是:每个模块至少为"纯逻辑单元"建一个*.test.ets(预计每个模块 5~15 条用例),公共模块(multishortvideobase)优先级最高——它的WidthBreakpointType、MSVDataModel、Logger被所有特性模块依赖,回归风险也最高。落地节奏上,不要求一次铺满,而是"新增代码必带用例、存量代码按覆盖率补测",两个迭代内把纯逻辑层覆盖到 80% 以上即可形成基本安全网。测试不是负担,而是重构与多设备适配的底气:没有用例兜底,"顺手改个断点"都可能变成线上事故。
七、总结与最佳实践
单元测试与 UI 测试的落地路径,归纳为五条最佳实践:
- 先测纯逻辑:
WidthBreakpointType、MSVDataModel、CommonConstants这类无 UI 依赖的单元是单测性价比最高的对象,优先补齐并锁定默认值与边界行为。 - 测试目录即规范:按模块建立
ohosTest/ets/test/*.test.ets,describe用被测类命名,it编号递增,断言统一expect。 - UI 测试锚定 id、等待转场:用组件
id而非文案定位(文案受国际化影响),异步场景显式等待;多设备用例至少覆盖两种断点。 - Mock 走 ViewModel 边界:工程 ViewModel 内置 mock 数据源,测试直接复用;外部能力(hilog、窗口)用注入或可替换引用打桩。
- 覆盖率牵引补测:以行覆盖率 80%(纯逻辑层)为牵引,优先补齐多设备差异分支,让每一次重构都有自动化回归兜底。