你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> [iOS單元測試系列]Singleton如何測試

[iOS單元測試系列]Singleton如何測試

編輯:IOS開發基礎

0.jpg

Singletion設計模式在cocoa中被廣泛使用。在我們平時寫App代碼時也經常會將一些工具類,管理類設計成Singletion。Signletion通過一個類方法返回一個唯一的實例,與我們平常通過實例化生成一個個實例的場景有所不同。如果我們要stub一個Singletion的類的實例方法,那麼這個Signletion的類初始化方法(eg:sharedMange())必須返回一個mock對象。因為只有mock對象才可以做stub操作。那麼我們應該如何mock我們的Singletion呢,我們通過下面的例子一步步分析解決這個問題。

Singleton場景

比如我有一個Singleton的類(DemoStatusManage),他有一個實例方法currentStatus會返回一個1-100的隨機數。

@interface DemoStatusManage : NSObject
+ (instancetype)sharedManage;
- (int)currentStatus;
@end
@implementation DemoStatusManage {
    NSInteger _status;
}
+ (instancetype)sharedManage {
    static dispatch_once_t once;
    static DemoStatusManage *manage;
    dispatch_once(&once, ^{
        manage = [[DemoStatusManage alloc] init];
    });
    return manage;
}
- (instancetype)init {
    self = [super init];
    if (self) {
        _status = 0;
    }
    return self;
}
- (int)currentStatus {
    return [self getRandomNumber:1 to:100];
}
-(int)getRandomNumber:(int)from to:(int)to {
    return (int)(from + (arc4random() % (to - from + 1)));
}
@end

然後在我的另外一個類中會去調用這個Singletion的currentStatus方法,並且將返回的數據渲染到另外那個類的label文案上。

- (void)updateStatusNumber {
    self.statusLabel.text = [NSString stringWithFormat:@"%ld",(long)[[DemoStatusManage sharedManage] currentStatus]];
}

這是一個很簡單的Singletion場景,但是在測試updateStatusNumber這個API的時候由於依賴到了外部的DemoStatusManage的currentStatus方法,而且這個方法返回的是一個隨機數值,所以我們必須mock掉Singletion,然後再stub調currentStatus方法,讓這個方法返回我們期望的一個固定值。

應該用OCMock的哪個API呢

應該用OCMock的哪個API呢?OCMStrictClassMock(cls)? OCMClassMock(cls)? OCMPartialMock(obj)?

其實這裡按照常規的mock測試一個API都用不上。因為我們mock出來的東西(對象或者是類)只能在我們的測試用例中,updateStatusNumber方法裡面調用的永遠是DemoStatusManage的原生類。

那如何才能讓sharedManage不管在哪裡(測試用例中和updateStatusNumber中)都返回我們的mock對象呢,答案是用category重寫sharedManage讓它返回我們的mock對象.

@interface DemoStatusManage (UnitTest)
@end
static DemoStatusManage *mock = nil;
@implementation DemoStatusManage (UnitTest)
+ (instancetype)sharedManage {
    if (mock)  return mock;
    static dispatch_once_t once;
    static DemoStatusManage *manage;
    dispatch_once(&once, ^{
        manage = [[DemoStatusManage alloc] init];
    });
    return manage;
}
@end

這樣在我們的單元測試類中只要在測試case中初始化一下mock,sharedManage不管在哪裡調用就都會返回我們需要的mock對象了。

mock = OCMClassMock([DemoStatusManage class]);

當然我們也可以讓mock返回一個PartialMock對象。

mock = OCMPartialMock([[DemoStatusManage alloc] init]);

包裝優化

去掉拷貝的代碼

你應該也發現了,這段代碼我們是拷貝過來的。

   static dispatch_once_t once;
   static DemoStatusManage *manage;
   dispatch_once(&once, ^{
       manage = [[DemoStatusManage alloc] init];
   });
   return manage;

如果用這種方式,我們會陷入一個問題,我們在維護兩套相同的代碼,那天app工程中相關的sharedManage的方法有所變動,這裡也要相應的變動。有什麼辦法可以讓它找到原來的IMP實現呢,Matt大神的一篇文章中就告訴我們,Yes,可以的!Supersequent implementation.我們可以用Matt的invokeSupersequentNoArgs()宏定義來實現這個功能。

這樣我們的Cagegory差不多就長這樣。

@interface DemoStatusManage (UnitTest)
@end
static DemoStatusManage *mock = nil;
@implementation DemoStatusManage (UnitTest)
+ (instancetype)sharedManage {
    if (mock)  return mock;
    return invokeSupersequentNoArgs()
}
@end

包裝mock方法

筆者在用這種方式寫測試用例的時候發現,可能我的UnitTest這個Category是寫在Atest.m中的,但是在沒有寫Category也沒有引用Atest.m的Btest.m中,也會進入到重寫的sharedManage中,而由於mock是static的,也沒有做釋放操作,導致DemoStatusManage永遠是一個mock對象。可能是因為XCTest框架的原因,因為所有的XCTestCase都是沒有.h文件的,具體原因也不得而知。

所以,要解決這個問題,我們必須在mock使用完畢後釋放它,並且將創建和釋放都包裝出來,提供接口給測試用例調用。而且我們可以提供不同類型的mock方式。

@interface DemoStatusManage (UnitTest)
+ (instancetype)JTKCreateClassMock;
+ (instancetype)JTKCreatePartialMock:(DemoStatusManage *)obj;
+ (void)JTKReleaseMock;
@end
static DemoStatusManage *mock = nil;
@implementation DemoStatusManage (UnitTest)
+ (instancetype)sharedManage {
    if (mock)  return mock;
    return invokeSupersequentNoArgs();
}
+ (instancetype)JTKCreateClassMock {
    mock = OCMClassMock([DemoStatusManage class]);
    return mock;
}
+ (instancetype)JTKCreatePartialMock:(DemoStatusManage *)obj {
    mock = OCMPartialMock(obj);
    return mock;
}
+ (void)JTKReleaseMock {
    mock = nil;
}
@end

這樣我們就可以在使用mock的時候調用JTKCreateClassMock 或者 JTKCreatePartialMock: 來生成我們需要的mock對象,在使用完畢後釋放我們的mock對象,就能實現我們的測試需求了。

宏定義簡化代碼

我們的工程中不可能只有一個Singletion,少則十幾,多則上百。如果我們對每個Singletion都這麼寫一遍Category的話,這個成本也太他媽大了。而其實不管是哪個Singletion,這個UnitTest的Category都是大同小異的,那麼我們不如寫個宏定義來簡化我們的代碼。

#define JTKMOCK_SINGLETON(__className,__sharedMethod)               \
JTKMOCK_SINGLETON_CATEGORY_DECLARE(__className)                     \
JTKMOCK_SINGLETON_CATEGORY_IMPLEMENT(__className,__sharedMethod)    \
#define JTKMOCK_SINGLETON_CATEGORY_DECLARE(__className)         \
                                                                \
@interface __className (UnitTest)                               \
                                                                \
+ (instancetype)JTKCreateClassMock;                             \
                                                                \
+ (instancetype)JTKCreatePartialMock:(__className *)obj;        \
                                                                \
+ (void)JTKReleaseMock;                                         \
                                                                \
@end
#define JTKMOCK_SINGLETON_CATEGORY_IMPLEMENT(__className,__sharedMethod)    \
                                                                            \
static __className *mock_singleton_##__className = nil;                     \
                                                                            \
@implementation __className (UnitTest)                                      \
                                                                            \
+ (instancetype)__sharedMethod {                                            \
    if (mock_singleton_##__className) return mock_singleton_##__className;  \
    return JTKInvokeSupersequentNoParameters();                             \
}                                                                           \
+ (instancetype)JTKCreateClassMock {                                        \
    mock_singleton_##__className = OCMClassMock([__className class]);       \
    return mock_singleton_##__className;                                    \
}                                                                           \
                                                                            \
+ (instancetype)JTKCreatePartialMock:(__className *)obj {                   \
    mock_singleton_##__className = OCMPartialMock(obj);                     \
    return mock_singleton_##__className;                                    \
}                                                                           \
                                                                            \
+ (void)JTKReleaseMock {                                                    \
    mock_singleton_##__className = nil;                                     \
}                                                                           \
                                                                            \
@end

這樣我們只需要一行代碼就能搞定一個Singletion的UnitTest的Category了,來一個寫一行,來一雙寫兩行。

JTKMOCK_SINGLETON(DemoStatusManage,sharedManage)

One more thing

Matt文中代碼可以在github上找到NSObject+SupersequentImplementation

如果使用invokeSupersequentNoArgs()提示Too many arguments to function call,expected 0,have 2,請打開你的測試工程的target,找到Build Setting下的Enable Strict Checking of objc_mesSend Calls,設置為NO

用category重寫主類中的方法會有一個警告:Category is implementing a method which will also be implemented by its primary class,則使用以下宏在你重寫的方法前後做個包裝即可

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
JTKMOCK_SINGLETON(DemoStatusManage,sharedManage)
#pragma clang diagnostic pop
  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved