- 文档
- 知识库
- 教程
- OpenHarmony
【免费下载链接】CangjieCorpus
本仓库为仓颉编程语言语料库,聚焦 仓颉编程语言 核心知识体系,整合官方开发指南、API 文档、典型代码示例及语法规范等多维度资源,经结构化处理与专业标注,构建为支持 RAG(检索增强生成)技术 的高质量知识基座,适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用,推动鸿蒙开发生态智能化升级!
本文基于 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): Nothingfail:使该用例失败,直接抛出异常,返回值类型为Nothing,对应 @Fail 宏——运行至此断言则整个测试用例失败,后续断言不再检查。
public func failExpect(message: String): UnitfailExpect:使该用例失败,记录信息但不抛出异常,返回值类型为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基本一致,差异有二:
- 回调函数签名为
(AssertionCtx) -> Any(返回值无类型约束); - 函数返回
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 testcjpm 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(检索增强生成)技术 的高质量知识基座,适配智能编码助手、学习辅助工具等应用开发需求。 欢迎开发者基于本语料库构建仓颉语言智能应用,推动鸿蒙开发生态智能化升级!
相关推荐
微信聊天记录导出工具WeChatMsg完整上手指南:把对话永久留存在你手里
微信聊天记录导出工具WeChatMsg完整上手指南:把对话永久留存在你手里 你有没有过这样的时刻:换了新手机,想翻看三年前和家人的一段对话,却发现记录早已被系统
Open Design技能开发完全手册:从SKILL.md到完整技能集成
Open Design技能开发完全手册:从SKILL.md到完整技能集成 想要在Open Design中创建自己的技能吗?🎯 本文为你提供 完整的技能开发指南
AI 应用人工智能AI 技能设计系统媒体生成Emscripten集成测试框架扩展:自定义断言
Emscripten集成测试框架扩展:自定义断言 引言 在Emscripten项目开发中,测试是确保代码质量和功能正确性的关键环节。Emscripten的测试框
编译器WebAssembly开发工具构建工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考