鸿蒙掌上驾考宝典应用开发44:单元测试与 UI 测试——ohosTest 实践
·
第44篇:单元测试与 UI 测试——ohosTest 实践

一、引言
在软件开发中,测试是保证代码质量和功能正确性的关键手段。高效的测试体系不仅可以减少 Bug 逃逸,还能在重构时提供安全网。DriverLicenseExam 项目在多个 HAR 模块中集成了 ohosTest 测试框架,涵盖了单元测试和集成测试。本文将深入解析项目的测试架构和实践。
二、ohosTest 测试框架概述
2.1 测试框架架构
ohosTest 是鸿蒙提供的官方测试框架,支持以下测试类型:
ohosTest 测试体系
├── 单元测试(Unit Test)
│ ├── 纯逻辑测试(不依赖 UI)
│ └── 数据服务测试
├── 集成测试(Integration Test)
│ ├── Ability 测试
│ └── 模块间交互测试
└── UI 测试(UI Test)
├── 组件渲染测试
└── 用户交互测试
2.2 测试目录结构
项目中每个 HAR 模块都包含测试目录:
commons/
├── commonLib/
│ └── src/
│ ├── ohosTest/ets/test/ ← 集成测试(运行在真机/模拟器)
│ │ ├── Ability.test.ets
│ │ └── List.test.ets
│ └── test/ ← 单元测试(可本地运行)
│ ├── List.test.ets
│ └── LocalUnit.test.ets
├── datasource/
│ └── src/
│ ├── ohosTest/ets/test/
│ │ ├── Ability.test.ets
│ │ └── List.test.ets
│ └── test/
│ ├── List.test.ets
│ └── LocalUnit.test.ets
└── network/
└── src/
├── ohosTest/ets/test/
│ ├── Ability.test.ets
│ └── List.test.ets
└── test/
├── List.test.ets
└── LocalUnit.test.ets
两个测试目录的区别:
| 目录 | 运行环境 | 测试类型 | 适用场景 |
|---|---|---|---|
src/test/ |
本地 JVM | 单元测试 | 纯逻辑、无 UI 依赖 |
src/ohosTest/ |
真机/模拟器 | 集成测试 | 依赖系统 API、UI |
三、单元测试实践
3.1 测试框架初始化
单元测试基于 @ohos/hypium 测试框架,使用 describe 和 it 组织测试用例:
// LocalUnit.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { ExamService } from '@ohos_agcit/driver_license_exam_datasource';
export default function examServiceTest() {
describe('ExamServiceTest', () => {
// 测试用例写在这里
});
}
3.2 数据服务测试
ExamService 是应用的核心数据服务,也是最值得编写测试的模块:
describe('ExamServiceTest', () => {
// 测试总题数
it('getTotalCount_should_return_correct_count', 0, () => {
const service = ExamService.instance(getContext());
const count = service.getTotalCount();
// 验证总题数是否正确
expect(count).assertEqual(10);
});
// 测试正确率(无已做题时应返回0)
it('calAccuracyRate_should_return_zero_when_no_questions_done', 0, () => {
const service = ExamService.instance(getContext());
const rate = service.calAccuracyRate();
expect(rate).assertEqual(0);
});
// 测试已做题数
it('getDidCount_should_return_number_of_answered_questions', 0, () => {
const service = ExamService.instance(getContext());
const didCount = service.getDidCount();
// 初始状态下应该为 0
expect(didCount).assertEqual(0);
});
// 测试平均分(无考试记录时应返回0)
it('calculateAverageScore_should_return_zero_when_no_exams', 0, () => {
const service = ExamService.instance(getContext());
const avgScore = service.calculateAverageScore();
expect(avgScore).assertEqual(0);
});
// 测试模拟考试记录
it('addMockExamCount_should_increase_count', 0, () => {
const service = ExamService.instance(getContext());
const before = service.mockExamCount;
service.addMockExamCount(90);
expect(service.mockExamCount).assertEqual(before + 1);
});
});
每个测试用例的命名遵循 methodName_expectedBehavior_when_condition 模式,让测试用例的意图一目了然。
3.3 网络 Mock 测试
网络层同样可以编写测试,但由于网络请求依赖外部服务,通常配合 Mock 使用:
describe('NetworkTest', () => {
it('mock_getUserInfo_should_return_mock_data', 0, () => {
const mockApi = new HttpApiMock();
mockApi.getUserInfo().then((resp) => {
expect(resp.code).assertEqual(0);
expect(resp.data.phone).assertEqual('1XXXXXX');
});
});
});
四、集成测试实践
4.1 Ability 测试
集成测试需要在真机或模拟器上运行,验证 Ability 的生命周期和功能:
// Ability.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { UIAbility } from '@kit.AbilityKit';
describe('EntryAbilityTest', () => {
it('onWindowStageCreate_should_load_content_successfully', 0, () => {
// 验证 Ability 初始化成功
// 注意:集成测试通常需要 Ability 测试框架的支持
});
it('setLightOrDarkMode_should_apply_correct_color_mode', 0, () => {
// 验证深色模式设置
});
});
4.2 列表测试
// List.test.ets
describe('MainListTest', () => {
it('home_tab_should_display_correctly', 0, () => {
// 验证首页 Tab 是否正确显示
});
it('exam_list_should_render_questions', 0, () => {
// 验证考试列表是否正确渲染
});
});
五、测试覆盖率分析
5.1 建议测试覆盖的重点模块
| 模块 | 优先级 | 测试重点 |
|---|---|---|
| ExamService | 🔴 高 | 数据统计、试卷生成、筛选逻辑 |
| AccountUtil | 🔴 高 | 登录/登出、信息管理 |
| PermissionUtil | 🟡 中 | 权限申请、状态检查 |
| RouterModule | 🟡 中 | 路由跳转、参数传递 |
| PreferencesUtil | 🟢 低 | 数据读写、缓存管理 |
| UI 组件 | 🟢 低 | 渲染测试、交互测试 |
5.2 边界情况测试
好的测试不仅要覆盖正常路径,更要覆盖边界情况:
describe('ExamServiceEdgeCases', () => {
it('calAccuracyRate_should_not_divide_by_zero', 0, () => {
const service = ExamService.instance(getContext());
// 极端情况:总题数为0
expect(service.calAccuracyRate()).assertEqual(0);
});
it('calculateAverageScore_should_handle_empty_array', 0, () => {
const service = ExamService.instance(getContext());
// 极端情况:无考试记录
expect(service.calculateAverageScore()).assertEqual(0);
});
});
六、测试最佳实践
6.1 AAA 模式
每个测试用例都应该遵循 Arrange-Act-Assert 模式:
it('getCorrectCount_should_count_correct_answers', 0, () => {
// Arrange - 准备测试数据
const service = ExamService.instance(getContext());
// Act - 执行测试操作(标记一道题正确)
service.examDetails[0].isCorrect = true;
// Assert - 验证结果
expect(service.getCorrectCount()).assertEqual(2);
});
6.2 测试隔离
每个测试用例应该独立运行,互不影响:
// 在每个测试之前重置状态
beforeEach(() => {
// 重新获取 ExamService 实例
});
// 避免测试用例之间共享可变状态
it('test1', () => { ... });
it('test2', () => { ... }); // 不依赖于 test1 的结果
七、总结
测试是工程化的重要组成部分。DriverLicenseExam 项目通过 ohosTest 框架构建了多层次的测试体系:
- 单元测试:验证 ExamService、HttpApi 等核心服务的逻辑正确性
- 集成测试:验证 Ability 生命周期、模块间协作
- 边界测试:覆盖除零异常、空数据等极端情况
测试体系的建立虽然需要投入额外的时间,但它带来的收益是长期的:
- 回归保障:修改代码时快速发现破坏性变更
- 重构信心:有测试覆盖的代码可以放心重构
- 文档作用:好的测试用例同时也是可执行的文档
关键源码文件:
commons/commonLib/src/test/LocalUnit.test.ets— 单元测试示例commons/datasource/src/ohosTest/ets/test/Ability.test.ets— Ability 测试commons/network/src/test/List.test.ets— 列表测试
更多推荐




所有评论(0)