在iOS中使用OCMock测试void方法

Nil*_*wal 5 testing unit-testing ocmock ios

我正在尝试为Objective-C类中的方法编写一个测试用例,该方法返回void.该方法mobileTest只是创建了另一个类对象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
Run Code Online (Sandbox Code Playgroud)

上述测试用例如下:

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

Ger*_*ero 5

如果不深入了解 OCMock 的用途以及它意味着什么测试设计范例,就没有简单的答案。

简而言之:您开始就不应该这样测试。测试应将测试方法视为黑匣子,并且仅比较和验证输入与输出。

更长的版本:基本上,您正在尝试测试副作用,因为它makeNoise没有执行HelloWorldClass甚至注册(或“看到”)的任何操作。或者用更积极的方式来说:只要在为您编写的测试中makeNoise进行了正确的测试,您就不需要测试那个简单的调用。 AnotherClass

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

在这种情况下,您应该始终问自己“这是验证这一点的正确位置吗?” 如果答案是“是”,则意味着您想要测试的任何消息都需要调用到不完全位于测试类范围内的对象。例如,它可以是单例(某些全局日志记录类)或该类的属性。然后你就有了一个句柄,一个可以正确模拟的对象(例如,将属性设置为部分模拟的对象)并验证。

在极少数情况下,可能会导致您为对象提供句柄/属性,以便能够在测试期间将其替换为模拟,但这通常表明类和/或方法设计不是最优的(我不会声称但情况总是如此)。

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

示例 1:验证 URL 是否已打开(在移动 Safari 中):这基本上是验证openURL:在共享NSApplication实例上调用的 URL,这与您的想法非常相似。请注意,共享实例并不“完全在测试方法范围内”,因为它是单例”

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

请注意,这是由于部分模拟的具体情况而起作用的:即使没有在模拟上调用,因为模拟与测试方法中使用的同一实例openURL:有关系,它仍然可以验证调用。如果实例不是无法工作的单例,则您将无法从方法中使用的同一对象创建模拟。

示例 2:修改代码版本以允许“抓取”内部对象。

@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
Run Code Online (Sandbox Code Playgroud)

现在测试:

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

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

示例 3:如果AnotherClass有一个非标准的初始化程序(如单例,或者来自工厂类),您可以存根它并返回一个模拟对象(这是一种脑结,但我已经使用了它)

@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]);
}
Run Code Online (Sandbox Code Playgroud)

这看起来有点愚蠢,我承认它很丑陋,但我在一些测试中使用了它(你知道项目中的这一点......)。在这里,我试图使其更易于阅读,并使用了两个模拟,一个用于存根类方法(即初始化程序),另一个则返回。然后,该mobileTest方法显然应该使用 的自定义初始值设定项AnotherClass,然后它获取模拟对象(如布谷鸟的蛋......)。如果您想专门准备对象,这非常有用(这就是我在这里使用部分模拟的原因)。我实际上不确定 atm 是否也可以仅使用一个类模拟来完成此操作(将类方法/初始化器存根在其上,以便它返回自身,然后期望您想要验证的方法调用)...正如我所说,大脑-打结。