鸿蒙多功能工具箱开发实战(二十四)-单元测试与自动化测试
前言
测试是保证代码质量的重要手段。本文将讲解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 测试进行中

六、小结
本文详细讲解了测试方法:
- ✅ 测试目录结构
- ✅ 单元测试编写
- ✅ 工具类测试
- ✅ UI组件测试
- ✅ 测试执行与报告
- ✅ Mock数据
- ✅ 测试工具函数
系列文章导航 :
下期预告:鸿蒙多功能工具箱开发实战(二十五)-国际化与多语言支持