测试驱动开发:在鸿蒙项目中实践TDD(59)

在鸿蒙(HarmonyOS)项目中实践测试驱动开发(TDD),核心在于遵循"红-绿-重构"的循环。鸿蒙提供了完善的测试工具链,开发者可以利用 DevEco Studio 内置的 DevEco Testing 框架,将测试分为无需设备的本地单元测试(Local Test)和需要真机或模拟器的端到端测试(Instrument Test)。

一、 TDD 核心循环与鸿蒙测试策略

在鸿蒙中实践 TDD,建议采用分层测试策略:

  1. 底层(Local Test):针对纯业务逻辑(如算法、工具类),直接在本地运行,速度极快,是 TDD 的主战场。
  2. 中层(Instrument Test):针对 UI 交互和系统 API 调用,在设备或模拟器上运行,验证真实用户行为。
  3. 高层(分布式测试):验证跨设备协同能力(如跨屏流转)。

二、 实战演练:以"计算器加法"为例

1. 编写失败的测试(Red)

test/ 目录下创建 Calculator.test.ets,先编写针对加法功能的测试用例。此时由于业务代码尚未实现,测试预期会失败。

javascript 复制代码
import { describe, it, expect } from '@ohos.unittest';
import { Calculator } from '../main/ets/utils/Calculator';

describe('CalculatorTests', () => {
  it('add_positive_numbers_should_return_correct_sum', 0, () => {
    const calculator = new Calculator();
    const result = calculator.add(1, 2);
    expect(result).assertEqual(3);
  });
});
2. 编写最简业务代码(Green)

Calculator.ets 中实现 add 方法,使测试通过。

javascript 复制代码
export class Calculator {
  add(a: number, b: number): number {
    return a + b;
  }
}
3. 处理异步逻辑与 Mock 数据

鸿蒙中大量 API 是异步的。在 TDD 中,通常需要 Mock 外部依赖(如网络请求)来隔离测试。

javascript 复制代码
import { describe, it, expect } from '@ohos.unittest';

describe('UserServiceTests', () => {
  // 模拟用户数据
  const mockUser = { id: 1, name: 'TestUser', age: 25, roles: ['developer'] };

  it('user_should_have_valid_role', 0, () => {
    // 使用 Mock 数据进行纯逻辑验证
    const hasRole = mockUser.roles.includes('developer');
    expect(hasRole).assertTrue();
  });

  it('async_data_fetch_should_return_data', 0, async () => {
    // 异步测试示例
    const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
    await delay(100);
    expect(true).assertTrue();
  });
});

三、 进阶:UI 自动化测试(Instrument Test)

当业务逻辑测试通过后,需要通过 UI 测试验证界面交互。在 DevEco Studio 中,右键 .ets 文件选择 Create Instrument Test 即可生成模板。

javascript 复制代码
import { describe, it, expect } from '@ohos/hypium';
import { ON, BY } from '@ohos/uitest';

describe('LoginTest', () => {
  it('login_success', async () => {
    // 1. 定位组件并模拟用户输入
    const username = await ON(BY.id('username_input')).find();
    await username.inputText('admin');

    // 2. 断言结果
    expect(await username.getText()).assertEqual('admin');
  });
});

四、 鸿蒙 TDD 最佳实践与工具链

  1. 善用可视化录制:对于复杂的 UI 测试,DevEco Studio 提供了"录制回放"功能。你可以手动在设备上操作,IDE 会自动生成对应的 ArkTS 测试脚本,大幅降低编写 UI 测试的成本。
  2. 集成 CI/CD 流水线 :将测试自动化。通过命令行 hvigorw test -p module=entry 执行测试,并将其接入 Jenkins 或 GitLab CI,实现每次代码提交后的自动质量验证。
  3. 应用质量体检 :除了单元测试,建议结合 DevEco Studio 的 AppAnalyzer 工具(Tools -> AppAnalyzer)进行静态分析、性能分析和上架前体检,提前发现兼容性和功耗问题。
  4. 轻量级第三方框架 :如果官方框架配置较重,或者需要跨平台测试,可以考虑社区驱动的 Python 库 hmdriver2,它语法简洁,非常适合快速迭代的 UI 自动化测试。

五、 测试架构设计:引入 MVVM 与依赖注入

TDD 的痛点往往在于"代码难以测试"。在鸿蒙 ArkTS 中,强烈建议采用 MVVM 架构,将 UI(View)、业务逻辑(ViewModel)和数据源(Model)严格分离。

  • View 层:只负责 UI 渲染,不包含业务逻辑。
  • ViewModel 层:通过接口(Interface)依赖 Model 层,而不是直接实例化。
  • Model 层:负责网络请求或本地存储。

TDD 优势:由于 ViewModel 依赖的是接口,我们在测试时可以轻松传入 Mock 对象,无需启动真实的网络或数据库,实现真正的"纯逻辑单元测试"。

六、 Mock 进阶:隔离外部依赖

在真实业务中,接口调用通常是异步的。在 TDD 中,我们需要拦截这些异步操作。

typescript

编辑

复制代码
// 1. 定义数据源接口
export interface IUserDataSource {
  getUserInfo(): Promise<User>;
}

// 2. 在 ViewModel 中依赖接口(而非具体实现)
export class UserViewModel {
  private dataSource: IUserDataSource;
  
  constructor(dataSource: IUserDataSource) {
    this.dataSource = dataSource;
  }

  async loadUser() {
    return await this.dataSource.getUserInfo();
  }
}

// 3. 在单元测试中编写 Mock 实现
describe('UserViewModel Tests', () => {
  it('should_return_user_when_loadUser_called', async () => {
    // 构造 Mock 对象
    const mockDataSource: IUserDataSource = {
      getUserInfo: async () => ({ id: 1, name: 'MockUser' })
    };
    
    // 注入 Mock 对象进行测试
    const viewModel = new UserViewModel(mockDataSource);
    const user = await viewModel.loadUser();
    
    expect(user.name).assertEqual('MockUser');
  });
});

七、 UI 自动化测试(Instrument Test)进阶

UI 测试运行在真实设备或模拟器上,主要用于验证"用户交互"和"组件状态"。

javascript 复制代码
import { describe, it, expect } from '@ohos/hypium';
import { ON, BY } from '@ohos/uitest';

describe('LoginUITest', () => {
  it('login_success_should_navigate_to_home', async () => {
    // 1. 模拟用户输入
    const usernameInput = await ON(BY.id('username_input')).find();
    await usernameInput.inputText('admin');
    
    const passwordInput = await ON(BY.id('password_input')).find();
    await passwordInput.inputText('123456');
    
    // 2. 模拟点击按钮
    const loginBtn = await ON(BY.id('login_btn')).find();
    await loginBtn.click();
    
    // 3. 断言 UI 状态(验证是否跳转或出现成功提示)
    const homePage = await ON(BY.id('home_page')).find();
    expect(homePage.exists()).assertTrue();
  });
});

八、 工程化:CI/CD 与自动化流水线

TDD 的价值在于"持续反馈"。必须将测试集成到自动化流水线中:

  1. 命令行执行 :使用 hvigorw test -p module=entry 触发测试。
  2. 生成覆盖率报告 :在 build-profile.json5 中开启覆盖率统计,生成 HTML 报告,监控核心代码的测试覆盖率(建议核心业务逻辑覆盖率 > 80%)。
  3. 门禁机制:在 GitLab CI / Jenkins 中配置 Pipeline,如果单元测试失败或覆盖率不达标,直接拦截 Merge Request,禁止合入主干。

九、 鸿蒙 TDD 避坑

  1. 避免过度 Mock UI 组件:UI 组件的测试应交给 Instrument Test,不要在 Local Test 中强行测试 UI 渲染。
  2. 慎用全局状态 :ArkTS 中的 AppStoragePersistentStorage 在单元测试中难以隔离。尽量通过参数传递状态,而不是依赖全局变量。
  3. 测试命名规范 :采用 方法名_场景_预期结果 的命名方式(如 add_negativeNumbers_shouldReturnCorrectSum),让测试用例本身成为最好的文档。