HarmonyOS开发:断言与验证Assertion深度使用
HarmonyOS开发:断言与验证Assertion深度使用
📌 核心要点:断言是测试的灵魂——没有断言的测试只是"跑了一下代码",根本不算测试。掌握Hypium提供的全套断言API,知道什么时候用哪个,才能写出真正有验证力的测试。
一、背景与动机
你见过那种测试吗——跑了一堆代码,最后一行console.log(result),然后人工看输出对不对?这不叫测试,这叫"手动验证的自动化版本"。
测试的核心价值在于自动判断对错,而断言就是做这件事的工具。assertEqual(result, 5)——如果result不是5,测试立即失败,你不用盯着屏幕看输出。一个没有断言的测试函数,和一段普通代码没有任何区别。
但断言不只是assertEqual那么简单。你要验证数组包含某个元素怎么办?要验证函数抛出了特定异常怎么办?要验证两个浮点数"足够接近"怎么办?要验证对象的部分属性怎么办?这些场景都需要不同的断言策略。用错了断言,要么验证不够严格(漏了Bug),要么验证太死板(正常重构也报错)。
二、核心原理
2.1 断言分类体系
flowchart TB
A[断言体系] --> B[相等性断言]
A --> C[布尔断言]
A --> D[异常断言]
A --> E[包含性断言]
A --> F[近似断言]
A --> G[自定义断言]
B --> B1[assertEqual]
B --> B2[assertNotEqual]
B --> B3[assertNull]
B --> B4[assertNotNull]
C --> C1[assertTrue]
C --> C2[assertFalse]
D --> D1[assertThrowError]
E --> E1[assertContain]
E --> E2[assertNotContain]
F --> F1[assertClose]
G --> G1[assertThat + Matcher]
G --> G2[自定义匹配器]
classDef mainStyle fill:#4CAF50,stroke:#388E3C,color:#fff,font-weight:bold
classDef equalStyle fill:#3498DB,stroke:#2980B9,color:#fff
classDef boolStyle fill:#2ECC71,stroke:#27AE60,color:#fff
classDef errorStyle fill:#F44336,stroke:#D32F2F,color:#fff
classDef containStyle fill:#F39C12,stroke:#E67E22,color:#fff
classDef closeStyle fill:#9B59B6,stroke:#8E44AD,color:#fff
classDef customStyle fill:#1ABC9C,stroke:#16A085,color:#fff
class A mainStyle
class B,B1,B2,B3,B4 equalStyle
class C,C1,C2 boolStyle
class D,D1 errorStyle
class E,E1,E2 containStyle
class F,F1 closeStyle
class G,G1,G2 customStyle
2.2 断言执行机制
断言失败时的行为取决于断言类型:
| 断言类型 | 失败行为 | 当前用例 | 后续用例 |
|---|---|---|---|
| 普通断言 | 抛出AssertionError | 立即终止 | 继续执行 |
| 软断言 | 记录失败 | 继续执行 | 继续执行 |
| 验证断言 | 记录失败 | 继续执行 | 继续执行 |
普通断言失败后,当前用例中后续代码不会执行。如果你需要在一个用例中验证多个条件且不想因为一个失败就中断,需要使用软断言模式。
2.3 断言选择决策树
你要验证什么?
├── 值相等 → assertEqual / assertNotEqual
├── 布尔条件 → assertTrue / assertFalse
├── 空值 → assertNull / assertNotNull
├── 抛出异常 → assertThrowError
├── 包含关系 → assertContain / assertNotContain
├── 浮点数近似 → assertClose
├── 对象部分属性 → 自定义匹配器
└── 复杂条件 → assertThat + 自定义Matcher
三、代码实战
3.1 基础用法:核心断言API
// AssertionDemo.ets - 断言演示
import { describe, it } from '@ohos/hypium'
export default function assertionDemoTest() {
describe('核心断言API演示', () => {
describe('assertEqual相等性断言', () => {
it('assertEqual_数值相等_通过', 0, () => {
assertEqual(1 + 1, 2)
})
it('assertEqual_字符串相等_通过', 0, () => {
assertEqual('hello'.toUpperCase(), 'HELLO')
})
it('assertNotEqual_不等_通过', 0, () => {
assertNotEqual(1 + 1, 3)
})
it('assertNull_值为null_通过', 0, () => {
let value: string | null = null
assertNull(value)
})
it('assertNotNull_值不为null_通过', 0, () => {
let value: string | null = 'hello'
assertNotNull(value)
})
})
describe('布尔断言', () => {
it('assertTrue_条件为真_通过', 0, () => {
assertTrue(5 > 3)
})
it('assertFalse_条件为假_通过', 0, () => {
assertFalse(5 < 3)
})
it('assertTrue_数组包含_通过', 0, () => {
const arr = [1, 2, 3]
assertTrue(arr.includes(2))
})
it('assertTrue_字符串匹配_通过', 0, () => {
const str = 'hello world'
assertTrue(str.startsWith('hello'))
})
})
describe('assertContain包含性断言', () => {
it('assertContain_数组包含元素_通过', 0, () => {
assertContain([1, 2, 3], 2)
})
it('assertContain_字符串包含子串_通过', 0, () => {
assertContain('hello world', 'world')
})
it('assertNotContain_不包含_通过', 0, () => {
assertNotContain([1, 2, 3], 4)
})
})
})
}
3.2 进阶用法:异常断言与近似断言
// TemperatureConverter.ets - 业务代码
export class TemperatureConverter {
static celsiusToFahrenheit(celsius: number): number {
if (celsius < -273.15) {
throw new Error('温度不能低于绝对零度(-273.15°C)')
}
return celsius * 9 / 5 + 32
}
static fahrenheitToCelsius(fahrenheit: number): number {
const celsius = (fahrenheit - 32) * 5 / 9
if (celsius < -273.15) {
throw new Error('温度不能低于绝对零度')
}
return celsius
}
static celsiusToKelvin(celsius: number): number {
if (celsius < -273.15) {
throw new Error('温度不能低于绝对零度(-273.15°C)')
}
return celsius + 273.15
}
}
// TemperatureConverterTest.ets - 异常断言与近似断言
import { describe, it } from '@ohos/hypium'
import { TemperatureConverter } from '../../../main/ets/utils/TemperatureConverter'
export default function temperatureConverterTest() {
describe('TemperatureConverter温度转换', () => {
describe('celsiusToFahrenheit摄氏转华氏', () => {
it('celsiusToFahrenheit_零度_32华氏度', 0, () => {
const result = TemperatureConverter.celsiusToFahrenheit(0)
assertClose(result, 32, 0.01)
})
it('celsiusToFahrenheit_100度_212华氏度', 0, () => {
const result = TemperatureConverter.celsiusToFahrenheit(100)
assertClose(result, 212, 0.01)
})
it('celsiusToFahrenheit_负40度_负40华氏度', 0, () => {
const result = TemperatureConverter.celsiusToFahrenheit(-40)
assertClose(result, -40, 0.01)
})
it('celsiusToFahrenheit_低于绝对零度_抛出异常', 0, () => {
assertThrowError(() => {
TemperatureConverter.celsiusToFahrenheit(-300)
})
})
it('celsiusToFahrenheit_刚好绝对零度_不抛异常', 0, () => {
const result = TemperatureConverter.celsiusToFahrenheit(-273.15)
assertClose(result, -459.67, 0.01)
})
})
describe('celsiusToKelvin摄氏转开尔文', () => {
it('celsiusToKelvin_零度_273.15开尔文', 0, () => {
const result = TemperatureConverter.celsiusToKelvin(0)
assertClose(result, 273.15, 0.01)
})
it('celsiusToKelvin_绝对零度_0开尔文', 0, () => {
const result = TemperatureConverter.celsiusToKelvin(-273.15)
assertClose(result, 0, 0.01)
})
it('celsiusToKelvin_低于绝对零度_抛出异常', 0, () => {
assertThrowError(() => {
TemperatureConverter.celsiusToKelvin(-274)
})
})
})
})
}
3.3 完整示例:自定义断言与软断言
// CustomAssertions.ets - 自定义断言库
export class SoftAssert {
private failures: string[] = []
private passed: number = 0
assertEqual(actual: unknown, expected: unknown, message?: string): void {
if (actual !== expected) {
this.failures.push(message || `期望 ${expected},实际 ${actual}`)
} else {
this.passed++
}
}
assertTrue(condition: boolean, message?: string): void {
if (!condition) {
this.failures.push(message || '期望为true,实际为false')
} else {
this.passed++
}
}
assertClose(actual: number, expected: number, tolerance: number, message?: string): void {
if (Math.abs(actual - expected) > tolerance) {
this.failures.push(message || `期望 ${expected}±${tolerance},实际 ${actual}`)
} else {
this.passed++
}
}
assertContains(arr: unknown[], item: unknown, message?: string): void {
if (!arr.includes(item)) {
this.failures.push(message || `数组不包含 ${item}`)
} else {
this.passed++
}
}
assertMatchesPattern(str: string, pattern: RegExp, message?: string): void {
if (!pattern.test(str)) {
this.failures.push(message || `字符串 "${str}" 不匹配模式 ${pattern}`)
} else {
this.passed++
}
}
assertHasProperties(obj: object, props: string[], message?: string): void {
for (const prop of props) {
if (!(prop in obj)) {
this.failures.push(message || `对象缺少属性 ${prop}`)
} else {
this.passed++
}
}
}
getResult(): { allPassed: boolean; passedCount: number; failedCount: number; failures: string[] } {
return {
allPassed: this.failures.length === 0,
passedCount: this.passed,
failedCount: this.failures.length,
failures: [...this.failures]
}
}
reset(): void {
this.failures = []
this.passed = 0
}
}
// 自定义匹配器
export class Matcher<T> {
private actual: T
private negated: boolean = false
constructor(actual: T) {
this.actual = actual
}
get not(): Matcher<T> {
this.negated = !this.negated
return this
}
toBe(expected: T): boolean {
const result = this.actual === expected
return this.negated ? !result : result
}
toBeCloseTo(expected: number, tolerance: number = 0.01): boolean {
if (typeof this.actual !== 'number') return false
const result = Math.abs(this.actual - expected) <= tolerance
return this.negated ? !result : result
}
toBeGreaterThan(target: number): boolean {
if (typeof this.actual !== 'number') return false
const result = this.actual > target
return this.negated ? !result : result
}
toBeLessThan(target: number): boolean {
if (typeof this.actual !== 'number') return false
const result = this.actual < target
return this.negated ? !result : result
}
toContain(item: unknown): boolean {
if (Array.isArray(this.actual)) {
const result = this.actual.includes(item)
return this.negated ? !result : result
}
if (typeof this.actual === 'string') {
const result = this.actual.includes(item as string)
return this.negated ? !result : result
}
return false
}
toMatch(pattern: RegExp): boolean {
if (typeof this.actual !== 'string') return false
const result = pattern.test(this.actual)
return this.negated ? !result : result
}
}
export function assertThat<T>(actual: T): Matcher<T> {
return new Matcher(actual)
}
// CustomAssertionsTest.ets
import { describe, it, beforeEach } from '@ohos/hypium'
import { SoftAssert, assertThat } from '../../../main/ets/utils/CustomAssertions'
export default function customAssertionsTest() {
describe('自定义断言库', () => {
describe('SoftAssert软断言', () => {
let softAssert: SoftAssert
beforeEach(() => {
softAssert = new SoftAssert()
})
it('SoftAssert_多个断言_全部通过', 0, () => {
softAssert.assertEqual(1 + 1, 2)
softAssert.assertTrue(3 > 2)
softAssert.assertClose(0.1 + 0.2, 0.3, 0.001)
const result = softAssert.getResult()
assertTrue(result.allPassed)
assertEqual(result.passedCount, 3)
assertEqual(result.failedCount, 0)
})
it('SoftAssert_部分失败_记录所有失败', 0, () => {
softAssert.assertEqual(1 + 1, 3)
softAssert.assertTrue(false)
softAssert.assertEqual('a', 'a')
const result = softAssert.getResult()
assertFalse(result.allPassed)
assertEqual(result.failedCount, 2)
assertEqual(result.passedCount, 1)
assertEqual(result.failures.length, 2)
})
it('SoftAssert_reset_清空记录', 0, () => {
softAssert.assertEqual(1, 2)
softAssert.reset()
const result = softAssert.getResult()
assertEqual(result.passedCount, 0)
assertEqual(result.failedCount, 0)
})
})
describe('assertThat匹配器', () => {
it('toBe_值相等', 0, () => {
assertTrue(assertThat(5).toBe(5))
assertFalse(assertThat(5).toBe(6))
})
it('not_toBe_取反', 0, () => {
assertTrue(assertThat(5).not.toBe(6))
})
it('toBeCloseTo_浮点近似', 0, () => {
assertTrue(assertThat(0.1 + 0.2).toBeCloseTo(0.3, 0.001))
})
it('toBeGreaterThan_大于', 0, () => {
assertTrue(assertThat(10).toBeGreaterThan(5))
assertFalse(assertThat(3).toBeGreaterThan(5))
})
it('toContain_数组包含', 0, () => {
assertTrue(assertThat([1, 2, 3]).toContain(2))
assertTrue(assertThat('hello world').toContain('world'))
})
it('toMatch_正则匹配', 0, () => {
assertTrue(assertThat('test123').toMatch(/test\d+/))
assertFalse(assertThat('hello').toMatch(/^\d+$/))
})
})
})
}
四、踩坑与注意事项
坑点1:assertEqual比较的是引用
对于对象和数组,assertEqual比较的是引用,不是内容。assertEqual({a: 1}, {a: 1})会失败,因为这是两个不同的对象。比较对象内容需要先转成JSON字符串:assertEqual(JSON.stringify(obj1), JSON.stringify(obj2)),或者逐个属性断言。
坑点2:assertThrowError要包在箭头函数里
assertThrowError(() => someFunction())——必须包在箭头函数里,不能直接传函数调用的结果。如果你写assertThrowError(someFunction()),函数会先执行,异常在assertThrowError捕获之前就抛出了,测试直接报错。
坑点3:浮点数不能用assertEqual
assertEqual(0.1 + 0.2, 0.3)会失败。浮点数运算有精度误差,必须用assertClose(actual, expected, tolerance),容差根据业务精度需求设定。
坑点4:断言消息要写清楚
assertEqual(result, 5, '计算2+3的结果应为5')——第三个参数是失败时的提示信息。不写的话,报错只会显示"expected 5, got 3",你还得去翻代码才知道测的是什么。写了的话,一眼就知道哪个业务逻辑出了问题。
坑点5:别用try-catch代替assertThrowError
有人喜欢这么写:
try {
someFunction()
assertTrue(false, '应该抛出异常但没抛')
} catch (e) {
assertTrue(true)
}
这能跑,但assertThrowError更简洁,而且能验证异常类型和消息。能用专用断言就用专用断言,别自己造轮子。
坑点6:软断言别忘了最终校验
软断言记录了失败但不中断执行,这是它的优势。但如果你忘了在用例末尾调用getResult()并做最终断言,那所有失败都被静默吞掉了——测试永远"通过",等于没测。
五、HarmonyOS 6适配说明
API差异表
| 功能/接口 | HarmonyOS 5 | HarmonyOS 6 | 变更说明 |
|---|---|---|---|
| 正则断言 | 无 | assertMatch | 新增正则匹配断言 |
| 类型断言 | 无 | assertType | 新增类型检查断言 |
| 软断言 | 需自实现 | SoftAssert内置 | 框架级软断言支持 |
| 自定义匹配器 | 需自实现 | assertThat + Matchers | 内置流式匹配器 |
| 断言消息 | 简单文本 | 模板字符串 | 支持变量插值 |
行为变更
-
assertMatch正则断言:新增
assertMatch(actual, pattern),专门用于正则匹配验证,不用再绕道assertTrue(pattern.test(str))。 -
assertType类型断言:新增
assertType(value, 'string'),用于验证运行时类型,比typeof检查更语义化。 -
内置SoftAssert:框架级软断言,不再需要自己实现。用法:
const soft = new SoftAssert(); soft.assertEqual(...); soft.assertAll()。
适配代码
// HarmonyOS 6新断言API
import { describe, it, assertMatch, assertType, SoftAssert } from '@ohos/hypium'
export default function newAssertionTest() {
describe('HarmonyOS 6新断言', () => {
it('assertMatch_正则匹配', 0, () => {
assertMatch('hello123', /^hello\d+$/)
})
it('assertType_类型检查', 0, () => {
assertType('hello', 'string')
assertType(42, 'number')
assertType([], 'array')
})
it('SoftAssert_软断言', 0, () => {
const soft = new SoftAssert()
soft.assertEqual(1 + 1, 2)
soft.assertTrue(3 > 2)
soft.assertAll()
})
})
}
六、总结
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐ |
| 使用频率 | ⭐⭐⭐⭐⭐ |
| 重要程度 | ⭐⭐⭐⭐⭐ |
断言选对了,测试写起来事半功倍;选错了,要么漏掉Bug要么维护成本爆炸。记住几个原则:值相等用assertEqual,浮点数用assertClose,异常用assertThrowError,包含关系用assertContain,复杂条件用自定义匹配器。断言消息一定要写,这是未来你排查问题时的救命线索。好的断言,是测试和调试的桥梁。
- 点赞
- 收藏
- 关注作者
评论(0)