鸿蒙多功能工具箱开发实战(二十四)-单元测试与自动化测试

鸿蒙多功能工具箱开发实战(二十四)-单元测试与自动化测试

前言

测试是保证代码质量的重要手段。本文将讲解HarmonyOS应用的单元测试和自动化测试方法。

一、测试框架

1.1 测试目录结构

复制代码
entry/
├── src/
│   └── main/
│       └── ets/
└── src/
    └── ohosTest/
        └── ets/
            ├── test/
            │   ├── calculator/
            │   │   └── TaxCalculator.test.ets
            │   └── utils/
            │       └── DateUtil.test.ets
            └── testability/
                └── TestAbility.ets

1.2 测试配置

typescript 复制代码
// ohosTest/ets/testability/TestAbility.ets
import TestRunner from '@ohos.application.testRunner'
import AbilityDelegatorRegistry from '@ohos.application.abilityDelegatorRegistry'

export default class OpenHarmonyTestRunner implements TestRunner {
  onPrepare() {
    console.info('TestRunner onPrepare')
  }

  onRun() {
    console.info('TestRunner onRun')
    let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator()

    let testConfig = {
      bundleName: 'com.example.harmonytoolbox',
      moduleName: 'entry',
      abilityName: 'EntryAbility',
      testCaseName: 'test'
    }

    abilityDelegator.executeShellCommand('aa test -p com.example.harmonytoolbox -m entry -a EntryAbility', (err, data) => {
      console.info('Test result: ' + data.stdResult)
    })
  }
}

二、单元测试

2.1 测试用例编写

typescript 复制代码
// ohosTest/ets/test/calculator/TaxCalculator.test.ets
import { describe, it, expect } from '@ohos/hypium'
import { TaxCalculator } from '../../../../main/ets/utils/TaxCalculator'

export default function taxCalculatorTest() {
  describe('TaxCalculator', () => {
    // 测试基本计算
    it('should calculate basic tax correctly', () => {
      const result = TaxCalculator.calculate({
        salary: 10000,
        socialInsurance: 2000,
        specialDeduction: 1000,
        bonus: 0
      })

      // 应纳税所得额 = 10000 - 2000 - 5000 - 1000 = 2000
      // 应纳税额 = 2000 * 0.03 = 60
      expect(result.taxableIncome).assertEqual(2000)
      expect(result.tax).assertEqual(60)
    })

    // 测试边界情况
    it('should handle zero tax case', () => {
      const result = TaxCalculator.calculate({
        salary: 5000,
        socialInsurance: 0,
        specialDeduction: 0,
        bonus: 0
      })

      expect(result.taxableIncome).assertEqual(0)
      expect(result.tax).assertEqual(0)
    })

    // 测试高收入
    it('should calculate high income tax correctly', () => {
      const result = TaxCalculator.calculate({
        salary: 50000,
        socialInsurance: 5000,
        specialDeduction: 2000,
        bonus: 0
      })

      // 应纳税所得额 = 50000 - 5000 - 5000 - 2000 = 38000
      expect(result.taxableIncome).assertEqual(38000)
      expect(result.tax).assertClose(6490, 10)  // 允许误差
    })

    // 测试年终奖计算
    it('should calculate bonus tax correctly', () => {
      const result = TaxCalculator.calculateBonusTax(36000)

      // 36000 / 12 = 3000,适用3%税率
      expect(result.tax).assertEqual(1080)
    })
  })
}

2.2 工具类测试

typescript 复制代码
// ohosTest/ets/test/utils/DateUtil.test.ets
import { describe, it, expect } from '@ohos/hypium'
import { DateUtil } from '../../../../main/ets/utils/DateUtil'

export default function dateUtilTest() {
  describe('DateUtil', () => {
    // 测试天数差计算
    it('should calculate days diff correctly', () => {
      const date1 = new Date(2024, 0, 1)
      const date2 = new Date(2024, 0, 10)

      const diff = DateUtil.getDaysDiff(date1, date2)
      expect(diff).assertEqual(9)
    })

    // 测试闰年判断
    it('should detect leap year correctly', () => {
      expect(DateUtil.isLeapYear(2024)).assertTrue()
      expect(DateUtil.isLeapYear(2023)).assertFalse()
      expect(DateUtil.isLeapYear(2000)).assertTrue()
      expect(DateUtil.isLeapYear(1900)).assertFalse()
    })

    // 测试月份天数
    it('should get correct days in month', () => {
      expect(DateUtil.getDaysInMonth(2024, 2)).assertEqual(29)  // 闰年2月
      expect(DateUtil.getDaysInMonth(2023, 2)).assertEqual(28)  // 平年2月
      expect(DateUtil.getDaysInMonth(2024, 1)).assertEqual(31)  // 1月
    })
  })
}

三、UI测试

3.1 组件测试

typescript 复制代码
// ohosTest/ets/test/components/NavBar.test.ets
import { describe, it, expect, Driver, Component } from '@ohos/hypium'

export default function navBarTest() {
  describe('NavBar Component', () => {
    let driver: Driver

    it('should display title correctly', async () => {
      // 查找标题元素
      const title = await driver.findComponent(Component.byId('nav_title'))

      // 验证文本内容
      const text = await title.getText()
      expect(text).assertEqual('首页')
    })

    it('should trigger back event on click', async () => {
      // 查找返回按钮
      const backButton = await driver.findComponent(Component.byId('nav_back'))

      // 点击返回按钮
      await backButton.click()

      // 验证是否触发了返回事件
      // ...
    })
  })
}

3.2 页面测试

typescript 复制代码
// ohosTest/ets/test/pages/TaxCalculatorPage.test.ets
import { describe, it, expect, Driver } from '@ohos/hypium'

export default function taxCalculatorPageTest() {
  describe('TaxCalculatorPage', () => {
    let driver: Driver

    it('should calculate tax after input', async () => {
      // 导航到页面
      await driver.navigateTo('pages/calculator/TaxCalculator')

      // 输入工资
      const salaryInput = await driver.findComponent(Component.byId('salary_input'))
      await salaryInput.setText('15000')

      // 点击计算按钮
      const calcButton = await driver.findComponent(Component.byId('calculate_button'))
      await calcButton.click()

      // 等待结果
      await driver.waitFor(500)

      // 验证结果
      const result = await driver.findComponent(Component.byId('result_text'))
      const text = await result.getText()
      expect(text).assertContain('税后工资')
    })
  })
}

四、测试执行

4.1 运行测试

bash 复制代码
# 运行所有测试
hvigorw test

# 运行特定测试文件
hvigorw test --module entry@default --test-file TaxCalculator.test

# 生成测试报告
hvigorw test --coverage

4.2 测试报告

测试完成后会生成报告,包含:

  • 测试用例数量
  • 通过/失败数量
  • 覆盖率统计
  • 错误详情

五、高级测试技巧

5.1 测试流程图

#mermaid-svg-cwtdK292H0eJWmz0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cwtdK292H0eJWmz0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cwtdK292H0eJWmz0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cwtdK292H0eJWmz0 .error-icon{fill:#552222;}#mermaid-svg-cwtdK292H0eJWmz0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cwtdK292H0eJWmz0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cwtdK292H0eJWmz0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cwtdK292H0eJWmz0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cwtdK292H0eJWmz0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cwtdK292H0eJWmz0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cwtdK292H0eJWmz0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cwtdK292H0eJWmz0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cwtdK292H0eJWmz0 .marker.cross{stroke:#333333;}#mermaid-svg-cwtdK292H0eJWmz0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cwtdK292H0eJWmz0 p{margin:0;}#mermaid-svg-cwtdK292H0eJWmz0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cwtdK292H0eJWmz0 .cluster-label text{fill:#333;}#mermaid-svg-cwtdK292H0eJWmz0 .cluster-label span{color:#333;}#mermaid-svg-cwtdK292H0eJWmz0 .cluster-label span p{background-color:transparent;}#mermaid-svg-cwtdK292H0eJWmz0 .label text,#mermaid-svg-cwtdK292H0eJWmz0 span{fill:#333;color:#333;}#mermaid-svg-cwtdK292H0eJWmz0 .node rect,#mermaid-svg-cwtdK292H0eJWmz0 .node circle,#mermaid-svg-cwtdK292H0eJWmz0 .node ellipse,#mermaid-svg-cwtdK292H0eJWmz0 .node polygon,#mermaid-svg-cwtdK292H0eJWmz0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cwtdK292H0eJWmz0 .rough-node .label text,#mermaid-svg-cwtdK292H0eJWmz0 .node .label text,#mermaid-svg-cwtdK292H0eJWmz0 .image-shape .label,#mermaid-svg-cwtdK292H0eJWmz0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-cwtdK292H0eJWmz0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cwtdK292H0eJWmz0 .rough-node .label,#mermaid-svg-cwtdK292H0eJWmz0 .node .label,#mermaid-svg-cwtdK292H0eJWmz0 .image-shape .label,#mermaid-svg-cwtdK292H0eJWmz0 .icon-shape .label{text-align:center;}#mermaid-svg-cwtdK292H0eJWmz0 .node.clickable{cursor:pointer;}#mermaid-svg-cwtdK292H0eJWmz0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cwtdK292H0eJWmz0 .arrowheadPath{fill:#333333;}#mermaid-svg-cwtdK292H0eJWmz0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cwtdK292H0eJWmz0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cwtdK292H0eJWmz0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cwtdK292H0eJWmz0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cwtdK292H0eJWmz0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cwtdK292H0eJWmz0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cwtdK292H0eJWmz0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cwtdK292H0eJWmz0 .cluster text{fill:#333;}#mermaid-svg-cwtdK292H0eJWmz0 .cluster span{color:#333;}#mermaid-svg-cwtdK292H0eJWmz0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cwtdK292H0eJWmz0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cwtdK292H0eJWmz0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-cwtdK292H0eJWmz0 .icon-shape,#mermaid-svg-cwtdK292H0eJWmz0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cwtdK292H0eJWmz0 .icon-shape p,#mermaid-svg-cwtdK292H0eJWmz0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cwtdK292H0eJWmz0 .icon-shape .label rect,#mermaid-svg-cwtdK292H0eJWmz0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cwtdK292H0eJWmz0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cwtdK292H0eJWmz0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cwtdK292H0eJWmz0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

编写测试用例
执行测试
测试通过?
生成覆盖率报告
修复代码
提交代码

5.2 Mock数据

typescript 复制代码
// Mock天气服务
class MockWeatherService {
  static getWeather(): WeatherData {
    return {
      city: '北京',
      temperature: 25,
      condition: '晴',
      humidity: 60
    }
  }
}

// 在测试中使用
it('should display weather correctly', () => {
  const weather = MockWeatherService.getWeather()
  expect(weather.city).toBe('北京')
})

5.3 测试工具函数

typescript 复制代码
// 等待异步操作
async function waitFor(condition: () => boolean, timeout = 5000) {
  const start = Date.now()
  while (!condition()) {
    if (Date.now() - start > timeout) {
      throw new Error('Timeout')
    }
    await new Promise(r => setTimeout(r, 100))
  }
}

图1 测试进行中

六、小结

本文详细讲解了测试方法:

  1. ✅ 测试目录结构
  2. ✅ 单元测试编写
  3. ✅ 工具类测试
  4. ✅ UI组件测试
  5. ✅ 测试执行与报告
  6. ✅ Mock数据
  7. ✅ 测试工具函数

系列文章导航

下期预告:鸿蒙多功能工具箱开发实战(二十五)-国际化与多语言支持

相关推荐
youtootech1 小时前
HarmonyOS实战教程《台词拼图》(二)—— 响应式布局与断点系统
华为·harmonyos
Catrice02 小时前
HarmonyOS ArkTS 实战:实现一个电影追剧与观影记录应用(完整源码)
华为·harmonyos
●VON2 小时前
鸿蒙 PC Markdown 编辑器自由窗口:覆盖侧栏与响应式预算
安全·华为·编辑器·harmonyos·鸿蒙
SameX2 小时前
ArkTS 用 Preferences 存 App 配置的正确姿势 —— 从踩坑到 singleton 封装
harmonyos
懿路向前2 小时前
【HarmonyOS学习笔记】2026-07-24 | textProcessing 实体识别与踩坑实录
笔记·学习·边缘计算·harmonyos
贾伟康3 小时前
【笔下生辉|02】HarmonyOS ArkTS 素材库详情实战:组织例句、解释、收藏和练习入口
harmonyos·arkts·详情页·学习进度·收藏功能
独隅3 小时前
DevEco Code 在 Windows/MacOS 双系统上的完整使用指南
ide·人工智能·windows·macos·华为·harmonyos
●VON3 小时前
鸿蒙 PC Markdown 编辑器有界版本历史:沙箱快照、完整性校验与安全恢复
安全·华为·编辑器·harmonyos·鸿蒙
痕忆丶4 小时前
OpenHarmony北向开发基础之 沙箱机制+分布式文件
harmonyos