首页 / 资讯中心 / 文章详情

仓颉标准库 mock 框架基础概念与用法:从 mock/spy 对象到桩、预期与桩链

仓颉标准库 mock 框架基础概念与用法:从 mock/spy 对象到桩、预期与桩链 ★ FEATURED ARTICLE
编程语言语言运行时标准库垃圾回收并发编程【免费下载链接】cangjie_runtime仓颉编程语言运行时与标准库。项目地址https://gitcode.com/Cangjie/cangjie_runtime点击查看免费下载本篇技术指南围绕仓颉标准库std.unittest.mock的mock 框架基础概念与用法展开系统讲解如何通过mockT/spyT创建测试替身如何用On宏配置 API为成员、静态声明与顶层声明打桩以及参数匹配器、操作 API、预期cardinality与桩链continuation的完整语义。读完本文你将掌握在仓颉单元测试中消除外部依赖、模拟异常场景并精确控制桩调用次数与顺序的实战能力。本文以stdlib/doc/libs/std/unittest_mock/unittest_mock_samples/mock_framework_basics.md为骨架并结合仓库内stdlib/libs/std/unittest/mock/目录下的真实源码实现create.cj、action_selectors.cj、cardinality.cj、argument_matchers.cj、selectors.cj、macros.cj、configure.cj等进行纵深印证。创建 mock 对象mock 构造函数可以通过调用mockT和spyT函数来创建两种对象mock和spy其中T表示被 mock 的类或接口。public func mockT(): T public func spyT(objectToSpyOn: T): Tmock作为骨架对象默认不对成员进行任何操作——也就是说未打桩的成员调用会直接导致测试失败这正是“mock 即空壳”的设计意图。spy作为一种特殊的 mock 对象用于封装某个类或接口的当前实例。默认情况下spy 对象将其成员调用委托给底层对象。其他方面spy 和 mock 对象非常相似。只有类包括 final 类和 sealed 类和接口支持通过这种方式 mock。从源码看create.cj 中mockT()实际调用createT(Mock, []) { handler unsafe { createMockT(handler) }}而spyT(objectToSpyOn)则调用createT(Spy, []) { handler unsafe { createSpyT(handler, objectToSpyOn) }}。二者共用同一个createT工厂框架为对象生成唯一 ID、注册MockObject并将类型强制转换为T返回从而保证 mock/spy 对象拥有与真实对象签名一致的 API。关于 spy 与 mock 在使用层面的差异参见下文使用 spy 和 mock 对象关于如何 mock 顶级和静态声明参见顶级和静态声明。配置 API配置 API是框架的核心可以定义 mock/spy 对象成员或顶层/静态声明的行为或重新定义 spy 对象或顶层/静态声明的行为。配置 API的入口是On宏调用。On(storage.getComments(testId)).returns(testComments)示例中如果 mock 对象storage接收到getComments方法的调用并且指定了参数testId则返回testComments。如上行为即为打桩桩Stub模拟还未实现或无法在测试环境中执行的组件需在测试用例主体内部先定义。源码印证On 宏的实现位于 mockmacro/macros.cj它通过parseAndGenerate解析桩调用表达式再按调用形态分发到generateStubFunction/generateStubGetter/generateStubSetter最终在ConfigureMockconfigure.cj中生成对应的MethodActionSelector/GetterActionSelector/SetterActionSelector。由此可见On并非运行时魔法而是编译期宏展开后桥接到框架运行时配置的过程。如下声明类型可以被打桩类和接口的实例成员包括 final 成员静态函数、属性和字段顶层函数和变量以下声明不能打桩扩展成员Foreign 函数局部函数和变量构造器常量任意私有声明一个完整的桩声明包含以下部分On宏调用中描述的桩签名。用于描述桩行为的操作。可选用于设置预期的基数说明符cardinality specifier指定预期执行次数的表达式。可选续体continuation支持链式调用的表达式。mock 框架拦截匹配桩签名的调用并执行桩声明中指定的操作。顶级和静态声明与类或接口的成员不同要打桩静态成员或顶层函数或变量时不需要创建模拟对象。这些声明应该直接使用配置 API例如On宏进行打桩。如下是一个为顶层函数打桩的示例import std.unittest.mock.* import std.unittest.mock.mockmacro.* public class Entry { Entry(let id: Int64, let title: String, let description: String) {} static func parse(): Entry { Entry(1, 1, 1) } } public func loadLastEntryInCatalog(): Entry { return Entry.parse() } public func drawLastEntryWidget() { let lastEntry loadLastEntryInCatalog() // drawing... } Test class RightsTest { TestCase func removeLastEntry() { On(loadLastEntryInCatalog()).returns(Entry(1, Test entry, Test description)) drawLastEntryWidget() } }从源码看configure.cj 中定义了STATIC_DECLARATIONS_MOCK_KIND MockKind.Spy这意味着静态成员与顶层声明的打桩行为在框架内部按spy 语义处理——未打桩的声明会调用原始实现详见下文使用 spy 和 mock 对象中的说明。桩签名桩签名定义了与特定调用子集匹配的一组条件包括以下部分mock/spy 对象的引用必须是单个标识符。独立声明顶层或静态函数、变量不需要此部分成员以及独立声明的调用。特定格式的参数调用参见参数匹配器。签名可以匹配以下实体方法属性 getter属性 setter字段读操作字段写操作静态函数静态属性 getter静态属性 setter静态字段读操作静态字段写操作顶层函数顶层字段读操作顶层字段写操作只要对应声明被调用并且所有参数若有都与相应的参数匹配器匹配时桩签名就会匹配调用。方法的桩的签名结构mock object name.method name(argument matcher*)。On(foo.method(0, 1)) // 带参数 0 和 1 的方法调用 On(foo.method(param1: 0, param2: 1)) // 带命名参数的方法调用当桩属性 getter/setter 或字段读/写操作时使用mock object name.property or field name或mock object name.property or field name argument matcher。On(foo.prop) // 属性 getter On(foo.prop 3) // 参数为 3 的属性 setter对于顶层函数和静态函数签名是类似的顶层函数function name(argument matcher*)静态函数class name.static method name(argument matcher*)顶层变量和静态属性或字段的签名如下顶层变量top-level variable name或top-level variable name argument matcher静态属性或字段class name.static property/field name或class name.static property/field name argument matcher对运算符函数打桩时运算符的接收者必须是对 mock/spy 对象的单个引用而运算符的参数必须是参数匹配器。On(foo 3) // operator func 参数为 3 On(foo[0]) // operator func []参数为 0参数匹配器每个桩签名必须包含所有参数的参数匹配器。单个参数匹配器定义了一个条件用于接受所有可能参数值的某个子集。每个匹配器都是通过调用Matchers类的静态方法来定义的。例如Matchers.any()是一个允许任何参数的有效匹配器。为了方便起见提供省略Matcher.前缀的语法糖。预定义的匹配器包括匹配器描述语法糖any()任何参数_符号eq(value: Equatable)value结构相等structural equality对象的值相等不一定内存相同的参数允许使用单个identifier和常量字面量same(reference: Object)reference引用相等referential equality对象的引用相等内存相同的参数允许单个identifierofTypeT()仅匹配T类型的值-argThat(predicate: (T) - Bool)仅匹配由predicate筛选出的T类型的值-none()匹配选项类型的None值-如果使用单个标识符作为参数匹配器则优先选择结构相等的参数。如果方法有默认参数并且没有显式指定该参数则使用 any() 匹配器。示例let p mockPrinter() // 假设 print 采用 ToString 类型的单个参数。 On(p.print(_)) // 可以使用“_”特殊字符代替 any() 。 On(p.print(eq(foo))) // 只匹配“foo”参数。 On(p.print(foo)) // 常量字符串可以省略显式匹配器。 let predefined foo // 可以传递单个标识符而不是参数匹配器。 On(p.print(predefined)) // 如果类型相等则使用结构相等来匹配。 On(p.print(ofTypeBar())) // 仅匹配 Bar 类型的参数。 // 对于更复杂的匹配器鼓励使用以下模式。 let hasQuestionMark {arg: String arg.contains(?)} On(p.print(argThat(hasQuestionMark))) // 只匹配包含问号的字符串。正确选择函数重载依赖仓颉类型推断机制。可以使用ofType来解决与类型推断有关的编译时问题。源码印证这些匹配器全部定义在 argument_matchers.cj 的Matchers类中。其中any()返回AnyMatcher无条件接受一切参数eqT通过target arg做结构相等判断要求T : EquatableTsameT通过refEq(target, arg)做引用相等判断要求T : ObjectofTypeT()与argThatT(predicate)均基于TypedMatcherT与PredicateMatcherT实现。另外该文件还提供了argThatNot、capture、default等补充匹配器以及withDescription/forParameter这类用于改善错误报告的修饰方法。重要规则函数调用作为参数匹配器时会视为对匹配器的调用。On(foo.bar(calculateArgument())) // 不正确calculateArgument() 不是匹配器。 let expectedArgument calculateArgument() On(foo.bar(expectedArgument)) // 正确只要 expectedArgument 是可等价的和/或引用类型。操作 APImock 框架提供 API 来指定桩操作。触发桩后打桩声明会执行指定的操作。如果调用与相应的On宏调用指定的签名匹配则会触发桩。每个桩函数必须指定一个操作。On宏调用返回的ActionSelector子类型会定义可用操作。操作列表取决于所打桩的实体。源码印证action_selectors.cj 中ActionSelector是sealed abstract class其三个公开子类分别面向不同实体MethodActionSelectorTRet方法、GetterActionSelectorTRet属性 getter / 字段读、SetterActionSelectorTArg属性 setter / 字段写。每个选择器都只暴露与该实体语义相符的操作集合这是“操作列表取决于所打桩实体”的源码级依据。通用操作适用于所有桩的操作。throws(exception: Exception)抛出exception。throws(exceptionFactory: () - Exception)调用exceptionFactory去构造桩触发时抛出的异常。fails()如果触发了桩则测试失败。注意throws用于测试桩声明抛出异常时的系统行为。fails用于测试桩声明是否未被调用。On(service.request()).throws(TimeoutException())函数和属性/字段 Getter 和顶层变量读操作R表示对应成员的返回类型。returns()不做任何操作并返回()仅当R为Unit时可用。returns(value: R)返回value。returns(valueFactory: () - R)调用valueFactory去构造桩触发时返回的值注原文档此处笔误为“抛出”按源码语义应为构造返回值。returnsConsecutively(values: ArrayR)、returnsConsecutively(values: ArrayListR)触发桩时返回values中的下一个元素。On(foo.bar()).returns(2) // 返回 2 On(foo.bar()).returnsConsecutively(1, 2, 3) // 依次返回 123源码印证returns(valueFactory)在 action_selectors.cj 中实现为sc.setAction(Action.Return { factory() })即每次触发桩时重新调用工厂returnsConsecutively内部将values.toArray()后逐次iterator.next()弹出并自动把基数设置为Cardinality.times(copy.size)这正是它“依次返回且必须恰好调用 N 次”的语义来源。此外returns()返回 Unit通过extend MethodActionSelectorUnit提供。属性/字段 Setter 和顶层变量写操作doesNothing()忽略调用不做任何操作。类似于返回 Unit 的函数的returns()。更多信息详见 桩使用指南设置属性和字段和顶层变量。spy 操作对于 spy 对象可以使用其他操作来委托监控实例。callsOriginal()调用原始方法。getsOriginal()调用原始属性 getter 或获取原始实例中的字段值。setsOriginal()调用原始属性 setter 或设置原始实例中的字段值。源码印证callsOriginal/getsOriginal/setsOriginal在 action_selectors.cj 中都会先调用mustBeSpy(actionName)若当前对象是Mock而非Spy则抛出StubMisconfiguration错误信息为OnlySpyCanCallOriginal。也就是说这三个操作被错误地用于纯 mock 对象时会在编译期/运行时被框架主动拦截。预期定义桩时会隐式或显式地向桩附加预期。桩可以定义期望的基数。操作fails和returnsConsecutively除外返回CardinalitySelector的实例该实例可以使用基数说明符自定义预期。CardinalitySelector定义了如下函数once()atLeastOnce()anyTimes()times(expectedTimes: Int64)times(min!: Int64, max!: Int64)atLeastTimes(minTimesExpected: Int64)anyTimes说明符用于提升预期即如果桩从未被触发测试也不会失败。其他说明符都暗示了测试代码中特定桩的调用次数上下限。只要桩被触发的次数比预期的多测试就会立即失败。下限在测试代码执行完毕后进行检查。示例import std.unittest.mock.* import std.unittest.mock.mockmacro.* class Foo { func bar() {} } Test func tooFewInvocations() { let foo mockFoo() On(foo.bar()).returns().times(2) foo.bar() }输出Expectation failed Too few invocations for stub foo.bar() declared at example_test.cj:9. Required: exactly 2 times Actual: 1 Invocations handled by this stub occured at: example_test.cj:6源码印证Cardinalitycardinality.cj用min!与可选max!表达调用次数区间其check(timesInvoked)返回Less/Ok/More三种判定调用次数超过上限立即失败、低于下限在测试代码执行完毕后检查正是基于该判定逻辑。CardinalitySelector与Continuation的实现见 selectors.cjonce()与times(n)返回续体可继续链式then()而anyTimes()、atLeastOnce()、atLeastTimes()、times(min, max)返回Unit。如果没有自定义预期mock 框架使用默认预期操作默认期望基数允许自定义基数fails不可调用否returnsatLeastOnce是returnsConsecutivelytimes(values.size)否throwsatLeastOnce是doesNothingatLeastOnce是(calls/gets/sets)OriginalatLeastOnce是桩链returnsConsecutively操作once和times(n)基数说明符返回一个续体实例。顾名思义续体表示可以继续使用链即指定某操作在前一个操作完全完成时立即执行。续体本身只提供了一个返回新ActionSelector的then()函数。链上的所有操作都适用相同的规则。如果调用了then()则必须指定新的操作。总预期为各个链预期之和。如果在测试中指定了一个复杂的链那么链的所有部分都会被触发。On(foo.bar()).returnsConsecutively(1, 2, 3, 4) // 同下 On(foo.bar()).returnsConsecutively(1, 2).then().returnsConsecutively(3, 4)// 指定了一个桩总共必须调用 NUM_OF_RETRIES 次 On(service.request()).throws(TimeoutException()).times(NUM_OF_RETRIES - 1). // 请求会先超时几次 then().returns(response).once() // 第一个成功响应之后停止发送请求源码印证selectors.cj 中Continuation.then()会为链上的下一段操作新建一个Scenario并挂到同一StubChain上因此“总预期为各个链预期之和”链的每一段都会分别检查自己的基数预期。使用 spy 和 mock 对象spy对象和mock对象在配置上是类似的只不过 spy 对象监控的是当前实例。主要区别如下成员调用没有触发任何桩时spy 对象调用底层实例的原始实现mock 对象抛出运行时错误并且测试失败。mock 对象无需创建真正的依赖来测试 API只需配置特定测试场景所需的行为。spy 对象支持重写真实实例的可观察行为。只有通过 spy 对象引用的调用才会被拦截。创建 spy 对象对原始 spy 实例的引用无影响。spy 调用自己的方法不会被拦截。let serviceSpy spy(service) // 模拟超时继续使用真正的实现 On(serviceSpy.request()).throws(TimeoutException()).once().then().callsOriginal() // 测试代码必须使用serviceSpy引用注意静态成员或顶级函数/变量的打桩行为类似于 spy即对于未打桩的声明将调用原始成员或原顶级函数/变量而不是像 mock 中那样抛出异常。mock 依赖接口始终可以被 mock。从另一个包 mock 一个类时类本身和它的超类必须按特定方式编译即只能 mock 预编译库如stdlib中的接口不能 mock 类。使用 cjc 编译对于cjc来说mock 是通过--mock标志来控制的。如果想 mock 特定包中的类p添加--mockon标志到 cjc 进行调用。说明在编译依赖p的包时也必须添加此标志。在测试中使用 mock 对象cjc --test不需要额外的标志。使用 cjpm 编译cjpm会自动检测 mock 使用并生成正确的cjc调用确保可以从任何从源代码编译的包中 mock 类。还可以使用 cjpm 配置文件控制哪些包支持 mock。小结与进一步阅读本文覆盖了 mock 框架的完整基础骨架mockT/spyT的对象创建、On宏驱动的配置 API、桩签名的书写规则、Matchers参数匹配器、按实体区分的操作 API、基数说明符与默认预期表以及续体桩链的链接语义。这些概念也是理解 mock 框架高阶用法的前提完整的可运行示例与典型测试流程可参阅 mock 框架入门桩的优先级、重定义、多桩、共享桩、参数捕获与桩模式ReturnsDefaults/SyntheticFields等进阶模式可参阅 桩使用指南调用次数、特定参数与调用顺序的交互验证可参阅 mock 框架验证 API完整的公开 API函数、类、枚举、异常清单可参阅 std.unittest.mock 包总览。mock 框架的运行时实现集中在stdlib/libs/std/unittest/mock/与stdlib/libs/std/unittest/mock/mockmacro/目录下读者可结合源码进一步研究On宏的展开逻辑mockmacro/macros.cj、调用处理internal/call_handler.cj与失败报告failure_report.cj等内部机制。赞分享编程语言语言运行时标准库垃圾回收并发编程【免费下载链接】cangjie_runtime仓颉编程语言运行时与标准库。项目地址https://gitcode.com/Cangjie/cangjie_runtime点击查看免费下载相关推荐仓颉编程语言 Mock 测试框架基础从 mock/spy 对象到 On 桩配置与期望校验仓颉编程语言 Mock 测试框架基础从 mock/spy 对象到 On 桩配置与期望校验 导读 std.unittest.mock 是仓颉标准库中内置于单元编程语言语言运行时标准库垃圾回收并发编程仓颉标准库 std.unittest.mock 全解析Mock/Spy 对象、桩配置与行为验证实战指南仓颉标准库 std.unittest.mock 全解析Mock/Spy 对象、桩配置与行为验证实战指南 仓颉Cangjie标准库中的 std.unitte编程语言语言运行时标准库垃圾回收并发编程仓颉 mock 框架构造函数深度解析mock\T 与 spy\T 的创建、桩模式与实战用法仓颉 mock 框架构造函数深度解析mock\T 与 spy\T 的创建、桩模式与实战用法 本文围绕仓颉标准库 std.unittest.mock 包中的文档知识库教程OpenHarmony上一篇Kinovea运动分析软件实战指南从体育训练到科研应用的专业解决方案下一篇植物大战僵尸终极修改器PvZ Tools 2.7.4完整使用指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站