OCMock 3 Partial Mock:类方法和 objc 运行时

Posted

技术标签:

【中文标题】OCMock 3 Partial Mock:类方法和 objc 运行时【英文标题】:OCMock 3 Partial Mock: Class methods and the objc runtime 【发布时间】:2015-09-21 18:53:57 【问题描述】:

在部分模拟定义类方法的对象时,我在使用 OCMock 3 时遇到了一个有趣的问题。我不确定这是否是作为部分模拟参与的动态子类化或我对 objc 运行时的误解的问题。任何帮助将不胜感激。

作为运行测试和其他调试版本的一部分,我们使用 OmniFoundations 的 OBRuntimeCheck 对方法声明进行一些运行时验证。简而言之,其中一项检查尝试使用运行时来验证类型签名是否与跨继承和协议一致性的类方法匹配。这通过列出在运行时中注册的类来实现,并为每个类复制metaClass 的实例方法。对于 metaClass 中的每个 Method,如果它存在于 metaClass 的超类中,则比较类型签名。

在为 ocmock 替换选择器之一 ocmock_replaced_* 调用 metaClass 的超类上的 class_getInstanceMethod 时出现问题。测试崩溃EXC_BAD_INSTRUCTION code=EXC_i386_INVOP subcode=0x0no class for metaclass 记录在控制台中。给出的例子:

class_getInstanceMethod(metaSuperClass, NSSelectorFromString(@"ocmock_replaced_classMessage")) 

当部分模拟定义类方法的对象时,OCMock 3 框架似乎生成了一个动态子类,对模拟对象进行了一些 isa 调动,并对动态生成的类的元类进行了一些调动。

这种行为和崩溃是 OCMock 3 中的新功能,我真的不知道下一步该往哪里看。任何运行时专家都知道这里可能发生了什么?在查看代码时,我确实感到惊讶,用于模拟的动态生成的类正在将它的元类弄乱,但我不一定认为这是错误的。为了便于调试,我在 OCMock 的新分支中创建了一个简化的测试用例。碰撞测试可以在here找到。任何指导帮助将不胜感激。

【问题讨论】:

【参考方案1】:

我可能离这里很远,但我认为元类的超类是 NSObject(这就是为什么你通常可以在类对象上调用 NSObject 实例方法的原因)。我不确定您是否应该正常使用元类的超类。

一般来说,metaClass 存储了关于类方法的所有信息。因此,在元类上获取“实例”方法与在关联的常规类上获取类方法相同。运行时可以简单地取消引用实例的“isa”指针来查找方法列表来查找实例方法;对 Class 对象执行相同操作会获得元类(具有相同结构),因此相同的过程会导致找到类方法。

OCMock 将为任何部分模拟创建一个魔术子类,并将该实例上的类更改为新的子类,因此所有实例方法调配将特定于该实例。但是对于类方法,我认为它必须修改原始类本身——否则,在常规代码中对常规类方法的调用将不会被拦截。它保留原始实现的副本,以便当您在模拟上调用 -stopMocking 时,它可以恢复原始实现(添加的 ocmock_replaced* impl 仍将存在但不应再调用)。

您可以简单地忽略任何以“ocmock_replaced”开头的选择器,因为这实际上与您可能正在检查的实际代码无关。您可能还会有更好的运气将“class_getInstanceMethod(metaSuperClass,...”更改为“class_getClassMethod(regularSuperClass,...”)。我不确定您为什么会崩溃 - 我希望 class_getInstanceMethod(metaSuperClass, ...) 在大多数情况下只返回 NULL。

【讨论】:

谢谢,我可以忽略带有“ocmock_replaced”前缀的选择器,但我真的希望了解崩溃。 class_getInstanceMethod 优于 class_getClassMethod 的原因是,这是在共享代码中与对象调用一次以验证实例方法,然后与类调用第二次以验证类方法。看起来作为正在应用的部分模拟的一部分,ocmock 正在将模拟对象的元类与用于模拟的动态生成的子类的元类混合出来。我想知道这是否与它有关? Lindeberg 有趣的是,我部分模拟的对象是OCMockObjectWithClassMessage 的一个实例。当我在部分模拟该类的实例后查看该类的元类时,元类是生成的 OCMock 类OCMockObjectWithClassMessage-0x105010f98-497779155。当我得到那个元类的超类(我称之为 metaSuperClass)时,我会返回 OCMockObjectWithClassMessage 通过查看 ObjC 运行时源,该错误消息是当运行时出现元类但找不到关联的常规类对象时。如果您在创建新的运行时类后忘记 registerClassPair() 可能会发生这种情况,但 OCMock 似乎正确地做到了这一点。通常,Class 对象的 isa 指针是元类。你如何获得 metaSuperClass 指针?指针或运行时有一些奇怪的东西,或者在这种情况下指针不是真的有效。我不希望任何 ocmock_replaced 方法存在于超类中。 我在this commit 的第 164 和 167 行得到了 metaClass 和 metaSuperClass。本质上,metaClass 是通过在创建部分模拟的类上调用 object_getClass 来获得的,然后通过在 metaClass 上调用 class_getSuperclass 来获得 metaSuperClass。感谢您帮助我深入研究这一点。 @CarlLindberg 嗯,是的,看看 OCClassMockObject,他正在玩元类游戏,可能是为了避免 ocmock_replaced 实现保留在常规类中。这大概就是问题的根源。这是我更熟悉的 OCMock 2.x 的一个变化。尽管如此,该类已正确注册,所以我猜运行时可以找出给定元类的类。但我并不完全理解他现在使用的结构。

以上是关于OCMock 3 Partial Mock:类方法和 objc 运行时的主要内容,如果未能解决你的问题,请参考以下文章

如何使用OCMock验证在Objective C中不调用异步方法?

OCMock 中的存根类方法

如何使用 OCMock 测试类方法

所有调用的 OCMock 存根类方法

是否可以在静态方法中 OCMock 类调用?

正在测试的类的 OCMock 存根类方法