第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 测试框架,使用 describeit 组织测试用例:

// 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 框架构建了多层次的测试体系:

  1. 单元测试:验证 ExamService、HttpApi 等核心服务的逻辑正确性
  2. 集成测试:验证 Ability 生命周期、模块间协作
  3. 边界测试:覆盖除零异常、空数据等极端情况

测试体系的建立虽然需要投入额外的时间,但它带来的收益是长期的:

  • 回归保障:修改代码时快速发现破坏性变更
  • 重构信心:有测试覆盖的代码可以放心重构
  • 文档作用:好的测试用例同时也是可执行的文档

关键源码文件:

  • commons/commonLib/src/test/LocalUnit.test.ets — 单元测试示例
  • commons/datasource/src/ohosTest/ets/test/Ability.test.ets — Ability 测试
  • commons/network/src/test/List.test.ets — 列表测试
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐