使用 OCMock 发生意外崩溃,在 NSString 上模拟“mutableCopy”

Posted

技术标签:

【中文标题】使用 OCMock 发生意外崩溃,在 NSString 上模拟“mutableCopy”【英文标题】:Unexpected crash using OCMock, mocking 'mutableCopy' on an NSString 【发布时间】:2013-03-11 18:30:46 【问题描述】:

我正在尝试在 ios 上使用 OCMock 和 GHUnit 模拟对 mutableCopy 的调用。

尽管测试通过了,但我在清理过程中收到了 EXC_BAD_ACCESS 异常,我正在尝试找出原因。

看看这个。该测试表明可以在模拟NSString 上模拟mutableCopy。在这个测试中,我返回另一个NSString,而不是NSMutableString。这只是为了证明mutableCopy 期望被触发,并且测试通过了。

#import <GHUnitIOS/GHUnit.h>
#import "OCMock.h"

@interface TestItClass : GHTestCase @end
@implementation TestItClass

// Test that mutableCopy on an NSString is mockable.
- (void)test_1_mutableCopyOfString_shouldBeMockable_givenAStringIsReturned 
    NSString *string = [OCMockObject mockForClass:NSString.class];
    NSString *copy = @"foo";
    [(NSString *) [[(id) string expect] andReturn:copy] mutableCopy];

    // MutableCopy is mocked to return a string, not a mutable string!
    // This is clearly wrong from a static typing point of view, but
    // the test passes anyway, which is ok.
    NSMutableString *result = [string mutableCopy];
    GHAssertEquals(result, copy, nil);
    [(id)string verify];

现在我更改了模拟期望,以便 mutableCopy 现在返回 NSMutableString。测试仍然通过,但在测试结束时,我得到了 EXC_BAD_ACCESS 异常。

- (void)test_2_mutableCopyOfString_shouldBeMockable_givenAMutableStringIsReturned 
    NSString *string = [OCMockObject mockForClass:NSString.class];
    NSMutableString *copy = [@"foo" mutableCopy];
    [(NSString *) [[(id) string expect] andReturn:copy] mutableCopy];

    // Now mutableCopy is mocked to return a mutable string!
    // The test now blows up during the test teardown! Why?
    NSMutableString *foo = [string mutableCopy];
    GHAssertEquals(foo, copy, nil);
    [(id)string verify];


@end

在这两个测试中,verifies 工作,关于断言。这表明这两个测试都构造良好,并且模拟预期正在按预期触发。但是,第二个测试在拆解时因内存访问错误而失败:

Simulator session started with process 7496
Debugger attached to process 7496
2013-03-11 18:23:05.519 UnitTests[7496:c07] TestItClass/test_2_mutableCopyOfString_shouldBeMockable_givenAMutableStringIsReturned ✘ 0.00s
2013-03-11 18:23:06.466 UnitTests[7496:c07] Re-running: TestItClass/test_2_mutableCopyOfString_shouldBeMockable_givenAMutableStringIsReturned <GHTest: 0x7793340>
Exception: EXC_BAD_ACCESS (code=1, address=0x11dfe3ea))

您能告诉我为什么会发生这种情况吗?

谢谢, 乔

【问题讨论】:

请在问题中包含您的- (void)tearDown 方法 没有明确的拆解;它只是 GHUnit 提供的库存。 @Shineeth 实际上,看起来崩溃发生在 OCMock 类之一的拆解中。我已经在下面发布了部分答案。 你能解释一下你在这个测试中想要达到的目标吗?通常没有任何理由模拟具有明确定义行为的核心 API 方法。可能有更好的方法来实现您想要测试的内容。 嗨,克里斯托弗,当然,我同意。不幸的是,在这种情况下,相对于少数经过良好测试的外部构建器类,从输入生成了一个相当复杂的输出字符串。我不希望这些的实现细节成为对使用它们的类的测试的考虑。这要求我模拟它们与累积其效果的可变字符串的交互。不理想,但我不愿意放弃关注点的封装。 【参考方案1】:

您面临的问题是由 ARC 遵循Basic Memory Management Rules 引起的。具体如下:

您拥有自己创建的任何对象

您使用名称以“alloc”、“new”、“copy”或“mutableCopy”开头的方法创建对象(例如,alloc、newObject 或 mutableCopy)。

因此解决方案是查看调用选择器以确定是否为retain returnValue

我希望这会有所帮助。

【讨论】:

Zaygraveyard 提供的解释是对的;对于创建对象的方法,OCMock 的行为不正确。我现在已经实施了一个解决方案。见github.com/erikdoe/ocmock/commit/…。 这是我缺少的一点。我很高兴我们在 OCMock 中发现了这个错误,因此维护者已经修复了它。谢谢:)。【参考方案2】:

我正在了解正在发生的事情。我为自己编译了一个OCMock 的调试库,以便了解崩溃发生的位置。

这是我找到的。

在我的原始测试中,我调用andReturn: 来设置返回期望值:

NSMutableString *copy = [@"foo" mutableCopy];
[(NSString *) [[(id) string expect] andReturn:copy] mutableCopy];

这反过来又调用OCMReturnValueProvider 来存储copy 以便它可以在适当的时间返回:

@implementation OCMReturnValueProvider

- (id)initWithValue:(id)aValue

    self = [super init];
    returnValue = [aValue retain];
    return self;

此时调试器说aValue__NSCFString 类型。 (警钟在我脑海中响起;这不是通往底层字符串的免费桥梁吗?不是对NSMutableString 的引用)

接下来测试完成并通过。

但是,当OCMReturnValueProviderdealloc'd 时,现在会出现问题。

@implementation OCMReturnValueProvider
- (void)dealloc

    [returnValue release];
    [super dealloc];

当调用[returnValue release] 时发生崩溃; OCMReturnValueProvider 正试图释放它之前 retained 的 __NSCFString

接下来,我打开了 NSZombie 调试,这很有启发性:

2013-03-12 20:58:19.654 UnitTests[16667:c07] TestItClass/test_2_mutableCopyOfString_shouldBeMockable_givenAMutableStringIsReturned  
2013-03-12 20:58:21.778 UnitTests[16667:c07] Re-running: TestItClass/test_2_mutableCopyOfString_shouldBeMockable_givenAMutableStringIsReturned <GHTest: 0x4afc5fd0>
2013-03-12 20:58:21.780 UnitTests[16667:c07] *** -[CFString release]: message sent to deallocated instance 0x4b0b1fe0

malloc-history(Find Zombie 工具)正在帮助阐明这一点:

Category            Event Type  Ref Ct  Responsible Caller
CFString (mutable)  Malloc        1     -[TestItClass test_2_mutable...]
CFString (mutable)  Retain        2     -[OCMReturnValueProvider initWithValue:]
CFString (mutable)  Retain        3     -[TestItClass test_2_mutable...]
CFString (mutable)  Retain        4     -[TestItClass test_2_mutable...]
CFString (mutable)  Release       3     -[TestItClass test_2_mutable...]
CFString (mutable)  Release       2     -[TestItClass test_2_mutable...]
CFString (mutable)  Release       1     -[TestItClass test_2_mutable...]
CFString (mutable)  Release       0     -[TestItClass test_2_mutable...]
CFString (mutable)  Zombie       -1     -[OCMReturnValueProvider dealloc]

因此,测试类中的某些内容导致发布多于保留。为什么会这样?奇怪的!

【讨论】:

【参考方案3】:

经过更多调查,我发现了崩溃发生的原因。

我们再来看看测试:

- (void)test_2_mutableCopyOfString_shouldBeMockable_givenAMutableStringIsReturned 
    NSString *string = [OCMockObject mockForClass:NSString.class];
    NSMutableString *copy = [@"foo" mutableCopy];
    [(NSString *) [[(id) string expect] andReturn:copy] mutableCopy];

    NSMutableString *foo = [string mutableCopy];

发生的情况是编译器假设[string mutableCopy] 返回的对象是mutableCopyretained,所以当foodealloced 时,ARC 相当于[foo release]。这是一个问题,因为我们在andReturn: 内没有增加这个对象的引用计数。

我很困惑为什么我们在配置为由andReturn: 返回的其他对象时看不到这种行为。处理模拟响应的 OCMReturnValueProvider 不是 ARC 托管的,并且不保留返回值:

- (void)handleInvocation:(NSInvocation *)anInvocation

    [anInvocation setReturnValue:&returnValue];

所以,问题的解决方法是先发制人地retaining 在将返回值设置到NSInvocation 之前:

- (void)handleInvocation:(NSInvocation *)anInvocation

    [returnValue retain];
    [anInvocation setReturnValue:&returnValue];

这看起来像是OCMock 中的一个错误。但鉴于该问题并非在所有情况下都会发生,我不确定。我的修复工作有效,但现在冒着在可能不需要额外retain 的对象上泄漏内存的风险。但是,测试中的内存泄漏与未运行的测试相比,目前我可以接受。

【讨论】:

我已经向 OCMock 的维护者提出了这个修复方案。解决后会在这里报告。 我认为问题在于 mutableCopy 有点特别。通常,调用者有责任保留他们想要保留的对象。被调用的方法返回具有“中性”保留计数的对象。然而,MutableCopy 返回一个保留计数为 +1 的对象。我看到了拉取请求,但会尝试找到一个不会泄漏内存的解决方案。 Zaygraveyard 提供的解释是对的;对于创建对象的方法,OCMock 的行为不正确。我现在已经实施了一个解决方案。见github.com/erikdoe/ocmock/commit/…。

以上是关于使用 OCMock 发生意外崩溃,在 NSString 上模拟“mutableCopy”的主要内容,如果未能解决你的问题,请参考以下文章

在现代风格的 OCMock 中使用 ignoringNonObjectArgs 时崩溃

MacOS Catalina 10.15.4 版中的“Spotlight 意外退出”,安装 Xcode 12 后发生此崩溃

OCMock 对带有参数的方法进行 OCMock 并返回一个值

6种办法解决你的Finder持续崩溃的问题!

CoreData 在插入和删除时崩溃同时发生

如何调试意外以静默方式终止的 win32 进程?