在 iOS 中使用 OCMock 测试 void 方法

Testing void methods using OCMock in iOS

我正在尝试为 Objective-C class 中 returns 无效的方法编写测试用例。方法 mobileTest 只是创建了 class AnotherClass 的另一个对象并调用了方法 makeNoise。如何测试这个?

我尝试使用 OCMock 创建测试。创建了一个 AnotherClass 的模拟对象,并调用了 mobileTest 方法。显然,OCMVerify([mockObject makeNoise]) 不起作用,因为我没有在代码中的任何位置设置此对象。那么,在这种情况下如何测试?

@interface Hello
@end

@implementation HelloWorldClass()

-(void)mobileTest{
    AnotherClass *anotherClassObject = [AnotherClass alloc] init];
    [anotherClassObject makeNoise];
}
@end


@interface AnotherClass
@end

@implementation AnotherClass()
-(void) makeNoise{
    NSLog(@"Makes lot of noise");   
}
@end

上面的测试用例如下:

-(void)testMobileTest{
    id mockObject = OCMClassMock([AnotherClass class]);
    HelloWorldClass *helloWorldObject = [[HelloWorld alloc] init];
    [helloWorldObject mobileTest];
    OCMVerify([mockObject makeNoise]);
}

如果不深入了解 OCMock 的用途及其暗示的测试设计范例,就没有简单的答案。

简短版本是:您首先不应该这样测试。测试应将测试方法视为黑盒,并且仅比较和验证输入与输出。

更长的版本:你正在尝试测试副作用,基本上,因为 makeNoise 没有做任何 HelloWorldClass 甚至寄存器(或 "sees")。或者换一种更积极的方式:只要 makeNoise 在为 AnotherClass 编写的测试中被正确测试 你不需要测试那么简单称呼。

您提供的示例可能有点令人困惑,因为显然这不会在 mobileTest 的单元测试中留下任何有意义的测试,但考虑到您可能还会质疑为什么外包简单的 NSLog 首先调用另一个 class (当然,测试 NSLog 调用有点毫无意义)。当然,我知道您只是以此为例,并设想了一个更复杂的不同场景,您希望在其中确保发生特定的调用。

在这种情况下,你应该经常问自己 "Is this the correct place to verify this?" 如果答案是 "yes" 那应该意味着无论你想测试调用的消息都需要转到一个不是完全在测试范围内class而已。例如,它可以是单例(某些全局日志记录 class)或 class 的 属性。然后你有一个句柄,一个你可以正确模拟的对象(例如,将 属性 设置为部分模拟的对象)并验证。

在极少数情况下,这可能会导致您为对象提供 handle/property,只是为了能够在测试期间用模拟替换它,但这通常表示次优 class and/or 方法设计(不过我不会说情况总是如此)。

让我提供我的一个项目中的三个示例来说明与您的情况类似的事情:

示例 1:验证 URL 是否已打开(在移动 Safari 中):这基本上是验证 openURL: 在共享 NSApplication 实例,与您的想法非常相似。请注意,共享实例不是 "completely inside the tested methods scope",因为它是一个单例

id mockApp = OCMPartialMock([UIApplication sharedApplication]);
OCMExpect([mockApp openURL:[OCMArg any]]);
// call the method to test that should call openURL:
OCMVerify([mockApp openURL:[OCMArg any]]);

请注意,由于部分模拟的特殊性,此方法有效:即使未在模拟上调用 openURL:,因为模拟与 中使用的相同实例有关系经过测试的方法它仍然可以验证调用。如果该实例不是无法工作的单例,您将无法从您的方法中使用的同一对象创建模拟。

示例 2:修改后的代码版本以允许 "grabbing" 内部对象。

@interface HelloWorldClass
@property (nonatomic, strong) AnotherClass *lazyLoadedClass;
@end

@implementation HelloWorldClass()
// ...
// overridden getter
-(AnotherClass *)lazyLoadedClass {
    if (!_lazyLoadedClass) {
        _lazyLoadedClass = [[AnotherClass alloc] init];
    }
    return _lazyLoadedClass;
}
-(void)mobileTest{
    [self.lazyLoadedClass makeNoise];
}
@end

现在测试:

-(void)testMobileTest{
    HelloWorldClass *helloWorldObject = [[HelloWorld alloc] init];
    id mockObject = OCMPartialMock([helloWorldObject lazyLoadedClass]);
    OCMExpect([mockObject makeNoise]);
    [helloWorldObject mobileTest];
    OCMVerify([mockObject makeNoise]);
}

lazyLoadedClass 方法甚至可能在 class 扩展中,即 "private"。在那种情况下,只需将相应的类别定义复制到测试文件的顶部(我通常这样做,是的,IMO,基本上 "testing private methods" 的有效案例)。如果 AnotherClass 更复杂并且需要精心设置或其他东西,则此方法很有意义。通常像这样的东西会导致你首先遇到的场景,即它的复杂性使它不仅仅是一个助手,而不是在方法完成后被丢弃。这也会引导您获得更好的代码结构,因为您在单独的方法中有它的初始化程序,并且也可以相应地对其进行测试。

示例 3:如果 AnotherClass 有一个非标准的初始化器(比如单例,或者它来自工厂 class),你可以存根那和return一个模拟对象(这是一个脑结,但我已经用过了)

@implementation AnotherClass()
// ...
-(AnotherClass *)crazyInitializer { // this is in addition to the normal one...
    return [[AnotherClass alloc] init];
}
@end

-(void)testMobileTest{
    HelloWorldClass *helloWorldObject = [[HelloWorld alloc] init];
    id mockForStubbingClassMethod = OCMClassMock([AnotherClass class]);
    AnotherClass *baseForPartialMock = [[AnotherClass alloc] init];
    // maybe do something with it for test settup
    id mockObject = OCMPartialMock(baseForPartialMock);
    OCMStub([mockForStubbingClassMethod crazyInitializer]).andReturn(mockObject);
    OCMExpect([mockObject makeNoise]);
    [helloWorldObject mobileTest];
    OCMVerify([mockObject makeNoise]);
}

这看起来有点愚蠢,我承认它很丑陋,但我已经在一些测试中使用过它(你知道项目中的这一点......)。在这里,我试图让它更容易阅读并使用了两个模拟,一个用于存根 class 方法(即初始化器),另一个是 returned。 mobileTest 方法显然应该使用 AnotherClass 的自定义初始化程序,然后它获取模拟对象(就像布谷鸟的蛋......)。如果你想专门准备对象(这就是我在这里使用部分模拟的原因),这很有用。我实际上不确定 atm 你是否也可以只用一个 class 模拟来做到这一点(将 class method/initializer 存根在它上面所以它 returns 本身,然后期待方法打电话给你要验证)...正如我所说,脑结。