iOS中

2016-12-22 33 views
5

使用OCMock測試void的方法,我試圖寫一個測試用例在Objective-C類的方法,它返回void。方法mobileTest只是創建類AnotherClass的另一個對象,並調用方法makeNoise。如何測試這個?iOS中

我試圖用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]); 
} 

回答

2

這裏沒有一個簡單的答案,這沒有去有點成什麼OCMock是爲哪些測試設計範式這意味着。

簡短版本是:你不應該首先像這樣測試它。測試應該將測試方法視爲黑盒子,並只比較輸入與輸出。

加長版:你想測試的一個副作用,基本上,因爲makeNoise沒有做任何的HelloWorldClass甚至寄存器(或「看到」)。或把它放在一個更積極的方式:只要makeNoise正確測試爲AnotherClass你不需要測試簡單的調用編寫了測試

您提供可能會有點混亂,因爲很明顯,不留任何有意義的mobileTest的單元測試,測試,不過考慮到你還可能會質疑爲什麼簡單NSLog呼叫外包給另一個類中的例子第一個地方(並且測試NSLog電話當然是毫無意義的)。當然,我明白你只是以此爲例,並設想一個更復雜的不同場景,在這種場景中你要確保發生特定的呼叫。

在這種情況下,你應該總是問自己「這是爲了驗證這個正確的地方嗎?」如果答案是肯定的,那應該暗示你想要測試的任何消息被調用需要去一個不完全在被測試類範圍內的對象。例如,它可能是一個單例(某些全局日誌類)或該類的一個屬性。然後你有一個句柄,一個可以正確模擬的對象(例如,將該屬性設置爲部分模擬對象)並進行驗證。

在極少數情況下,可能會導致您爲對象提供句柄/屬性,只是爲了能夠在測試過程中用模擬替換它,但這通常表示次優類和/或方法設計(I'儘管如此,我不會認爲情況總是如此)。

讓我提供了從我的項目之一三個例子來說明類似的情況了一句:

例1:驗證一個URL打開(在移動Safari瀏覽器):這基本上驗證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]]); 

注意這是由於部分模擬的細節:即使openURL:不叫上模擬因爲模擬具有在被測試的方法來同一個實例的關係它仍然可以驗證該呼叫。如果實例不是那就沒辦法了單身,你將無法創建一個從模擬在您的方法中使用的相同對象

示例2:改編版本的代碼以允許「gra bbing「內部對象。

@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方法,甚至可能在類延伸,即 「私人」。在這種情況下,只複製根據類別定義,測試文件的頂部(我通常這樣做,是的,這就是,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]); 
} 

這看起來挺傻的,我承認這是醜陋的,但我已經在一些測試中(你知道一個項目,點...)用這個。在這裏,我試圖使它更易於閱讀和使用的兩個嘲笑,一個存根類方法(即初始化),另一個則返回。然後mobileTest方法顯然應當使用AnotherClass定製初始化,然後它會嘲笑對象(如布穀鳥的蛋...)。如果您想專門準備對象,這非常有用(這就是爲什麼我在這裏使用了部分模擬的原因)。我實際上並不確定atm是否也可以只用一個類模擬來完成這個工作(在它上面存根類方法/初始化程序,所以它會自動返回,然後期望你想驗證的方法調用)......正如我所說,打結。