HarmonyOS开发:断言与验证Assertion深度使用

举报
Jack20 发表于 2026/06/24 15:11:32 2026/06/24
【摘要】 HarmonyOS开发:断言与验证Assertion深度使用📌 核心要点:断言是测试的灵魂——没有断言的测试只是"跑了一下代码",根本不算测试。掌握Hypium提供的全套断言API,知道什么时候用哪个,才能写出真正有验证力的测试。 一、背景与动机你见过那种测试吗——跑了一堆代码,最后一行console.log(result),然后人工看输出对不对?这不叫测试,这叫"手动验证的自动化版本"...

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 内置流式匹配器
断言消息 简单文本 模板字符串 支持变量插值

行为变更

  1. assertMatch正则断言:新增assertMatch(actual, pattern),专门用于正则匹配验证,不用再绕道assertTrue(pattern.test(str))

  2. assertType类型断言:新增assertType(value, 'string'),用于验证运行时类型,比typeof检查更语义化。

  3. 内置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,复杂条件用自定义匹配器。断言消息一定要写,这是未来你排查问题时的救命线索。好的断言,是测试和调试的桥梁

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。