☰
仓颉 unittest 框架函数 API 详解:断言、自定义断言与测试入口
2026/10/15 16:34:26 网站建设 项目流程
  • 文档
  • 知识库
  • 教程
  • OpenHarmony

【免费下载链接】CangjieCorpus

本仓库为仓颉编程语言语料库,聚焦 仓颉编程语言 核心知识体系,整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源,经结构化处理与专业标注,构建为支持 RAG(检索增强生成)技术 的高质量知识基座,适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用,推动鸿蒙开发生态智能化升级!

项目地址:https://gitcode.com/Cangjie/CangjieCorpus
点击查看免费下载

本文基于 CangjieCorpus 仓库中 std.unittest 包函数 API 文档 展开,系统梳理仓颉单元测试框架std.unittest包对外暴露的 10 个核心函数:断言比较函数、异常预期校验函数、自定义断言驱动函数、测试入口函数与默认配置生成函数。读完本文,你将掌握每个函数的完整签名、参数语义与返回值约定,理解 assert 系列与 expect 系列在"快速失败 / 记录后继续"上的根本差异,并能将invokeCustomAssert/invokeCustomExpect与@CustomAssertion宏、AssertionCtx上下文对象串联起来编写用户自定义断言。

函数总览:std.unittest包的公开函数清单

std.unittest包用于编写仓颉项目单元测试代码,提供代码编写、运行和调测的基本功能,并支持cjc编译器(单包编译模式)与cjpm包管理器(多包模式)两种运行方式。包级概览见 unittest_package_overview.md,其中"函数"一节完整列出本包公开的 10 个函数,可归纳为四类:

函数功能类别
assertEqual<T>比较期望值与实际值是否相等,不等直接抛出异常断言(快速失败)
assertCaughtUnexpectedE捕获的异常不符合预期,记录信息并抛出异常异常断言(快速失败)
fail(String)使用例失败,直接抛出异常失败断言(快速失败)
expectEqual<T>比较期望值与实际值是否相等,记录结果不抛出异常断言(记录后继续)
expectCaughtUnexpectedE捕获的异常不符合预期,记录信息不抛出异常异常断言(记录后继续)
failExpect(String)使用例失败,记录信息不抛出异常失败断言(记录后继续)
invokeCustomAssert<T>运行@Assertcaller指定的用户定义断言自定义断言
invokeCustomExpect运行@Expectcaller指定的用户定义断言自定义断言
entryMain(TestPackage)提供给cjc --test使用的框架执行入口框架入口
defaultConfiguration()生成默认配置信息框架配置

其中,assert*与expect*两条主线分别对应单元测试中的**快速失败(fail-fast)与记录后继续(continue-on-failure)**两种失败处理策略,这一点在 unittest_basics.md 中有明确说明:@Assert宏一旦首次断言失败,整个测试用例即失败、后续断言不再检查;而@Expect宏会记录失败信息并继续执行后续断言。上述函数正是这些宏在运行期的底层支撑实现。

断言比较函数:assertEqual与expectEqual

相等断言是单元测试中使用频率最高的断言形式,包内提供一对语义互补的函数。

assertEqual<T>:不等即抛出异常

public func assertEqual<T>( leftStr: String, rightStr: String, expected: T, actual: T, optParentCtx!: ?AssertionCtx = None): Unit where T <: Equatable<T>

参数语义:

  • leftStr: String:期望的表达式的字符串(用于报告输出)。
  • rightStr: String:实际的表达式的字符串(用于报告输出)。
  • expected: T:期望的值。
  • actual: T:实际值。
  • optParentCtx!: ?AssertionCtx = None:存储嵌套断言失败消息的上下文,类型为Option<AssertionCtx>,AssertionCtx类定义见 unittest_package_classes.md。

泛型约束T <: Equatable<T>要求被比较的类型实现Equatable<T>接口,这与 unittest_basics.md 中对宏的约束完全一致:"@Assert(a, b)的两个参数a和b,假设a的类型为A,b的类型为B,A必须实现了Equatable<B>"。当expected与actual不相等时,该函数直接抛出异常,使当前用例立即失败——这正是@Assert(leftExpr, rightExpr)宏展开后的运行期行为。

expectEqual<T>:记录结果,不抛出异常

public func expectEqual<T>( leftStr: String, rightStr: String, expected: T, actual: T, optParentCtx!: ?AssertionCtx ): Unit where T <: Equatable<T>

与assertEqual的差别仅在于失败处理策略:expectEqual只记录比较结果、不抛出异常,用例继续执行。@Expect(leftExpr, rightExpr)宏即基于此实现。

两者的运行差异:一次真实的失败演示

在 unittest_basics.md 中给出了一个可复现的对照实验。定义函数add后,先用两个@Expect断言:

@Test func testAddIncorrect() { @Expect(add(3, 3), 5) @Expect(add(5, 3), 9) }

两个检查项都会出现在输出中,且各自给出左右值:

[ FAILED ] CASE: testAddIncorrect (4212 ns) Expect Failed: `(add ( 3 , 3 ) == 5)` left: 6 right: 5 Expect Failed: `(add ( 5 , 3 ) == 9)` left: 8 right: 9

而换成两个@Assert后:

@Test func testAddIncorrectAssert() { @Assert(add(3, 3), 5) @Assert(add(5, 3), 9) }

输出中只出现第一个失败:

[ FAILED ] CASE: testAddIncorrectAssert (30893 ns) Assert Failed: `(add ( 3 , 3 ) == 5)` left: 6 right: 5

原因在于@Assert采用的快速失败机制:一旦首次断言失败,整个测试用例失败,后续断言不再检查。在涉及大量断言的大型测试(尤其是循环中的断言)中,这种机制能让用户第一时间感知失败,避免浪费执行时间。选择哪种断言取决于测试场景的复杂程度以及是否需要快速失败。

异常断言函数:assertCaughtUnexpectedE与expectCaughtUnexpectedE

当测试期望某段代码抛出特定异常、但实际捕获到的异常与预期不符时,这两个函数负责把"不符合预期"这一事实记录下来。

assertCaughtUnexpectedE:抛出异常终止用例

public func assertCaughtUnexpectedE( message: String, expectedExceptions: String, caughtException: String, optParentCtx!: ?AssertionCtx = None ): Nothing

参数语义:

  • message: String:不符合预期时的提示信息。
  • expectedExceptions: String:期望捕获的异常(字符串表达,例如"Exception"或"IllegalStateException | NoneValueException")。
  • caughtException: String:实际捕获的异常(字符串表达)。
  • optParentCtx!: ?AssertionCtx = None:存储嵌套断言失败消息的上下文。

返回值类型为Nothing,表示该函数在异常不符合预期时必然抛出异常、不会正常返回。它对应 @AssertThrows 宏 的运行期行为:预期抛出的异常类型未被抛出时用例失败,且不再进行后续检查。

expectCaughtUnexpectedE:记录信息继续执行

public func expectCaughtUnexpectedE( message: String, expectedExceptions: String, caughtException: String, optParentCtx!: ?AssertionCtx = None ): Unit

与上一函数的唯一差异是返回值类型为Unit——记录失败信息但不抛出异常,对应 @ExpectThrows 宏 的运行期行为:失败后继续检查后续断言。

预期异常断言的宏语法参考

宏层面的预期异常断言语法(见 unittest_basics.md)有助于理解上面两个函数中expectedExceptions参数的语义:

// 预期抛出异常基类 Exception(无属性入参时) @AssertThrows(throw Exception()) // 语义上与 No.1 相同 @AssertThrowsException) // 通过 | 隔离多个预期异常类型 @AssertThrowsIllegalStateException | NoneValueException @ExpectThrowsOutOfMemoryError) // 传入代码块 @ExpectThrows({ foo() boo() })

值得注意的返回类型约定:@AssertThrows在预期异常不超过一个时,返回类型与预期抛出的异常类型相同(如let e: NoneValueException = @AssertThrowsNoneValueException));指定多个异常时,返回类型为各异常类型的最小公共超类型。@ExpectThrows检查失败后继续执行,异常不超过一个时返回?T(如?NoneValueException),超过一个时返回?Exception。

失败断言函数:fail与failExpect

当测试逻辑需要"无条件令用例失败"时(例如检测到异常状态、生成了不合法数据),使用这两个函数。

public func fail(message: String): Nothing
  • fail:使该用例失败,直接抛出异常,返回值类型为Nothing,对应 @Fail 宏——运行至此断言则整个测试用例失败,后续断言不再检查。
public func failExpect(message: String): Unit
  • failExpect:使该用例失败,记录信息但不抛出异常,返回值类型为Unit,对应 @FailExpect 宏——运行至此断言会使得用例失败,但后续断言依然会被检查。

unittest_basics.md 给出了@Fail的典型用法——校验随机数生成器是否生成了偶数:

@Test func validate_even_number_generator() { let even = generateRandomEven() if (even % 2 == 1) { @Fail("Not even number was generated: ${even}") } }

输出如下错误信息:

[ FAILED ] CASE: validate_even_number_generator (54313 ns) Assert Failed: `(Not even number was generated: 111)`

注意fail/failExpect的失败信息同样以断言失败的样式呈现,其底层异常为 AssertException——该异常正是 @Expect/@Assert 宏检查失败时所抛出的异常。

自定义断言驱动函数:invokeCustomAssert与invokeCustomExpect

std.unittest支持用户通过 @CustomAssertion 宏 定义自己的断言函数,然后在@Assertcaller或@Expectcaller中调用。invokeCustomAssert与invokeCustomExpect正是框架在运行期驱动这些自定义断言函数的执行引擎。

invokeCustomAssert<T>:驱动@Assert自定义断言

public func invokeCustomAssert<T>( passerdArgs: Array<String>, caller: String, assert: (AssertionCtx) -> T, optParentCtx!: ?AssertionCtx = None ): T

参数语义:

  • passedArgs: Array<String>:未处理的已传递参数(原始字符串形式)。
  • caller: String:调用的自定义断言的名称(即@Assert[caller]中的caller)。
  • assert: (AssertionCtx) -> T:捕获带有正确参数的断言调用——框架构造AssertionCtx后回调用户定义的断言函数体。
  • optParentCtx!: ?AssertionCtx = None:存储嵌套断言失败消息的上下文。

返回值:T,即用户定义的断言函数返回的值。这正是 @CustomAssertion 宏返回值约定 的实现基础:"@CustomAssertion修饰的函数存在返回值时,它将被@Assert宏返回",从而支持let value = @AssertcheckNotNone这样把断言结果直接赋给变量的写法。

invokeCustomExpect:驱动@Expect自定义断言

public func invokeCustomExpect( passerdArgs: Array<String>, caller: String, expect: (AssertionCtx) -> Any, optParentCtx!: ?AssertionCtx = None ): Unit

参数语义与invokeCustomAssert基本一致,差异有二:

  1. 回调函数签名为(AssertionCtx) -> Any(返回值无类型约束);
  2. 函数返回Unit——对应宏文档中的约定:"自定义@Expect将总是返回Unit,不论@CustomAssertion修饰的函数返回值为什么类型"(见 @CustomAssertion 宏返回值)。

AssertionCtx:自定义断言的状态容器

上述两个函数的回调参数类型AssertionCtx定义于 unittest_package_classes.md,其职责是"存储用户定义的断言的状态,提供用于编写用户定义断言的方法"。核心成员:

  • prop args: String:以逗号分隔的未解析的用户定义断言参数的字符串。
  • prop caller: String:用户定义的断言函数的标识符。
  • prop hasErrors: Bool:用户定义内任何断言失败时为true,否则为false。
  • func arg(key: String): String:返回与参数列表标识符匹配的原始参数值字符串表达。
  • func fail(message: String): Nothing:存储失败信息并抛出AssertException。
  • func fail<PP>(pt: PP): Nothing where PP <: PrettyPrintable:同上,接受实现PrettyPrintable的失败信息对象。
  • func failExpect(message: String): Unit与func failExpect<PP>(pp): Unit:存储失败信息但继续执行。
  • func setArgsAliases(aliases: Array<String>): Unit:框架内部使用,设置通过arg访问未解析参数时使用的别名。

完整自定义断言示例

综合 @CustomAssertion 宏文档 中的示例,一个检查Option非空的用户自定义断言可写为:

@CustomAssertion public func checkNotNone<T>(ctx: AssertionCtx, value: ?T): T { if (let Some(res) <- value) { return res } ctx.fail("Expected ${ctx.arg("value")} to be Some(_) but got None") }

测试侧通过@AssertcheckNotNone触发invokeCustomAssert,从而把checkNotNone作为断言执行:

@Test func customTest() { @AssertcheckNotNone }

失败输出呈树状结构,提升嵌套断言可读性:

[ FAILED ] CASE: customTest (120812 ns) Assert Failed: @AssertcheckNotNone └── Assert Failed: `('Option < Bool >.None' was expected to be Some(_) but got None)`

嵌套断言与泛型指定

在@CustomAssertion定义内部,@Assert/@Expect(包括自定义断言)、@AssertThrows/@ExpectThrows、@Fail/@FailExpect均可继续调用,形成嵌套。宏文档中的iterableWithoutNone示例展示了这种递归组合(见 嵌套断言):

@CustomAssertion func iterableWithoutNone<T>(ctx: AssertionCtx, iter: Interable<?T>): Array<T> { iter |> map { it: ?T => @AssertcheckNotNone} |> collectArray }

指定泛型类型参数时使用常规语法:@AssertdoesThrow<NoneValueException>})。

此外,宏文档还说明了两种失败模式的对偶行为:自定义断言被@Expect宏调用时抛出的AssertException会被捕获、不向外传递;被@Assert宏调用时若未引发异常,异常将被创建并抛出。

框架入口与默认配置:entryMain与defaultConfiguration

entryMain(TestPackage):cjc --test的入口

public func entryMain(testPackage: TestPackage): Int64

功能:提供给cjc --test使用,是框架执行测试用例的入口函数。

参数:

  • testPackage: TestPackage:测试包对象。TestPackage定义于 unittest_package_classes.md,其构造入参为包名称字符串,并提供了三个注册方法:registerCase(() -> UnitTestCase)注册单元测试用例、registerSuite(() -> TestSuite)注册测试套、registerBench(() -> Benchmark)注册性能用例,均以惰性闭包形式注册。

返回值:Int64,执行结果。该返回值即进程退出码的语义来源——测试全部通过时返回 0,存在失败用例时返回非 0 值。

配合 unittest_getting_started.md 的快速入门示例,entryMain的完整调用链如下:编写被测函数与测试文件后,用cjc以测试模式构建并运行:

cjc add.cj add_test.cj --test -o add_test ./add_test

其中--test表明在测试模式下构建生成二进制文件,entryMain就是该模式下生成的二进制程序的测试执行入口。对于多包的大型项目,则建议使用cjpm包管理器:

cjpm init example example cjpm test

cjpm test会运行项目所有包中的测试,无需指定--test或任何其他特殊选项——cjpm默认通过文件名确认编译模式,以_test.cj结尾的文件即测试文件(详见 unittest_getting_started.md)。

defaultConfiguration():默认配置生成

public func defaultConfiguration(): Configuration

功能:生成默认的配置信息。

返回值:Configuration。Configuration定义于 unittest_common_package_classes.md,是一个类似HashMap的键值容器,但键为KeyFor<T>类型、值为任意给定类型,用于存储 @Configure 宏 生成的unittest配置数据。其常用方法包括:

  • get<T>(key: KeyFor<T>): ?T/getByName<T>(name: String): ?T:按键或名称取值,未找到时返回None。
  • set<T>(key: KeyFor<T>, value: T)/setByName<T>(name: String, value: T):设置值。
  • remove<T>(key: KeyFor<T>): ?T/removeByName<T>(name: String): ?T:删除值。
  • clone(): Configuration:拷贝配置对象。

在 @Configure 宏文档 中,当前支持的配置参数包括:randomSeed(Int64,随机种子)、generationSteps(Int64,参数化测试生成值的最大步数)、reductionSteps(Int64,缩减值的最大步数),以及主要用于@Bench性能测试的explicitGC(ExplicitGcType,默认Light)、baseline(String)、batchSize(Int64 或 Range )、minBatches(Int64,默认 10)、minDuration(Duration,默认 5 秒)、warmup(Duration 或 Int64,默认 1 秒)。

同时,unittest_basics.md 说明测试配置存在三种途径:@Configure宏、运行测试时(或cjpm test时)的命令行参数、以及cjpm配置文件。运行参数(如--filter、--include-tags、--parallel、--option=value等)最终也会转换为Configuration条目,命令行参数中option的连字符命名会转换为小驼峰格式,且其优先级低于代码中@Configure的优先级。defaultConfiguration()正是这些配置流转链路中生成初始默认值的函数。

与宏体系的对应关系:一张图理清函数定位

std.unittest的函数与unittest.testmacro包的宏存在严格的运行期对应关系,可归纳如下:

函数(std.unittest)对应宏(unittest.testmacro)失败行为
assertEqual@Assert(leftExpr, rightExpr)快速失败,抛出异常
expectEqual@Expect(leftExpr, rightExpr)记录后继续
assertCaughtUnexpectedE@AssertThrows快速失败,抛出异常
expectCaughtUnexpectedE@ExpectThrows记录后继续
fail@Fail快速失败,抛出异常
failExpect@FailExpect记录后继续
invokeCustomAssert@Assertcaller依自定义断言逻辑
invokeCustomExpect@Expectcaller依自定义断言逻辑

宏文档(unittest_testmacro_package_macros.md)还说明了断言的两种形式:@Assert(a, b)/@Expect(a, b)是相等断言(要求A <: Equatable<B>);@Assert(c)/@Expect(c)是布尔断言,可视为@Assert(c, true)/@Expect(c, true)的简写。在大型测试中,快速失败机制能显著缩短失败反馈时间,两种策略的选择取决于测试场景的复杂程度。

实战整合:从入门到自定义断言的完整示例

将上述函数串联起来的完整实战路径,可参考 unittest_getting_started.md 的入门示例(@Test+@Expect)与 unittest_parameterized_tests.md 的参数化测试(@Test[size in [0, 1, 50, 100, 1000]])。若需在程序内部直接运行测试而不经由cjc --test入口,可借助 动态测试 中的TestSuite.builder(...)与suite.runTests(),并通过Report对象配合报告器输出结果:

import std.unittest.* main() { let suiteBuilder = TestSuite.builder("A") suiteBuilder.add( UnitTestCase.create("f") { @Assert(false) }) let suite = suiteBuilder.build() let report = suite.runTests() report.reportTo(ConsoleReporter(colored: true)) }

Report报告基类(unittest_package_classes.md)提供caseCount、passedCount、failedCount、skippedCount、errorCount等统计属性;ConsoleReporter 支持彩色输出,XmlReporter、CsvReporter、CsvRawReporter 则分别将结果落盘为 XML 与 CSV 格式。

需要进一步深入时,建议依次阅读:

  • 包概览与重导出关系:unittest_package_overview.md
  • 宏定义与语法:unittest_testmacro_package_macros.md
  • 类与上下文对象:unittest_package_classes.md
  • 配置容器:unittest_common_package_classes.md
  • 入门与基础概念:unittest_getting_started.md、unittest_basics.md
  • 参数化测试:unittest_parameterized_tests.md
  • 动态测试:unittest_dynamic_tests.md
  • 文档
  • 知识库
  • 教程
  • OpenHarmony

【免费下载链接】CangjieCorpus

本仓库为仓颉编程语言语料库,聚焦 仓颉编程语言 核心知识体系,整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源,经结构化处理与专业标注,构建为支持 RAG(检索增强生成)技术 的高质量知识基座,适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用,推动鸿蒙开发生态智能化升级!

项目地址:https://gitcode.com/Cangjie/CangjieCorpus
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询