在iOS

时间:2016-12-22 03:41:15

标签: ios unit-testing testing ocmock

我正在尝试为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

上述测试用例如下:

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

1 个答案:

答案 0 :(得分:2)

如果没有深入了解OCMock的含义以及它所暗示的测试设计范例,那么这个答案并不是一个简单的答案。

简短版是:你不应该首先测试这个。测试应该将测试的方法视为一个黑盒子,只比较&验证输入与输出。

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

你提供的例子可能有点令人困惑,因为很明显在mobileTest的单元测试中没有留下任何有意义的东西,但考虑到你也可能会质疑为什么要外包简单的{{1首先调用另一个类(当然,测试NSLog调用是没有意义的)。当然我理解你只是以此为例,设想一个更复杂的不同场景,你要确保特定的调用发生。

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

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

让我从我的一个项目中提供三个例子来说明类似于你的情况:

示例1 :验证是否已打开网址(在移动版Safari中):这基本上是验证在共享NSLog实例上调用了openURL:,这是非常相似的你有什么想法。请注意,共享实例不是“完全在测试方法范围内”,因为它是单例“

NSApplication

请注意,这可以归功于部分模拟的细节:即使模拟没有调用id mockApp = OCMPartialMock([UIApplication sharedApplication]); OCMExpect([mockApp openURL:[OCMArg any]]); // call the method to test that should call openURL: OCMVerify([mockApp openURL:[OCMArg any]]); ,因为模拟与在测试方法中使用的相同实例有关系< / em>它仍然可以验证通话。如果实例不是一个不起作用的单例,那么您将无法从方法中使用的同一对象创建模拟。

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

openURL:

现在测试:

@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]); } 方法甚至可能属于类扩展,即“私有”。在这种情况下,只需将相应的类别定义复制到测试文件的顶部(我通常这样做,是的,这是IMO,基本上是“测试私有方法”的有效案例)。如果lazyLoadedClass更复杂并且需要精心设置或其他什么,这种方法是有意义的。通常这样的东西会导致你首先出现的场景,即它的复杂性使它不仅仅是一个帮助器,而不是在方法完成后丢弃。这将引导您获得更好的代码结构,因为您在一个单独的方法中有它的初始化程序,并且也可以相应地测试它。

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

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]); } 方法显然应该使用mobileTest的自定义初始化程序,然后它获取模拟对象(就像杜鹃的蛋...)。如果你想特别准备对象,这很有用(这就是我在这里使用部分模拟的原因)。我实际上不确定你是否也可以只使用一个类mock(将类方法/初始化器存在于它上面因此它返回自身,然后期望你要验证的方法调用)...正如我所说,脑 - 打结。