iOS Framework 單元測試(二)-- JDAppTests(XCTests的補充)

JiandanDream發表於2018-04-18

寫在前面

筆者在使用了 XCTests 對 Framework 進行單元測試過程中,發現無法使用 XCTests 進行真機測試,而專案剛好涉及到必須真機測試的功能。

所以簡單地做了個小工具,對其進行補充。

基本思路

目標:使用真機進行單元測試。

簡單粗暴的方式,就是建立一個 App 工程,然後將 Framework 工程直接拖到 App 工程裡。

優點:

  1. Framework 中的所有程式碼及資源,App 都可以訪問。
  2. 然後只要在 App 工程裡編寫測試程式碼就行。

缺點:

  1. 工程會比較複雜,甚至有點混亂,他人接手時,總會一頭霧水。
  2. 編寫測試程式碼時,因為會有很多測試用例,而且會不斷增加,如果每次都去修改 UI,修改呼叫方法,會浪費許多寶貴時間。

所以針對以上2個缺點,筆者對這種方案,進行了簡單的優化。

優化後方案:

  1. 在 Framework 工程中,新增一個 App Target,使用它來進行真機測試。
  2. 藉助 TableView,通過 Cell 去呼叫每個測試用例。
  3. 新增測試用例時,不需要改動原來程式碼。

如何做到上述第3點?

筆者想了一種簡單的實現方案:

  1. 建立一個測試基類 JDAppTestCase,所有測試類都去繼承它。
  2. JDAppTestCase 提供獲取所有子類的方法,這樣新增測試類,就不用去修改原來程式碼。
  3. JDAppTestCase 提供獲取『測試方法』的方法,這樣新增測試方法,也不用去修改原來程式碼。

模仿 XCTests,把這種方案稱為 AppTests。

所以關鍵在於 JDAppTestCase 這2個方法,該如何實現。

具體實現

建立一個 App Target 的過程,不再贅述。

建立完,需要新增對原來 Framework 工程的依賴,如圖:

AppTests引用Framework.png

JDAppTestCase 的實現

要獲取一個類的所有子類,網上有很多實現方式,主要是藉助 runtime,這裡引用了其中一種。

而要獲取『測試方法』,筆者約定:

  • 所有測試方法都以 test 開頭。
  • 每個 test 方法是一個測試用例。

然後藉助 _shortMethodDescription 這個私有方法獲得所有方法,最後篩選出測試方法。

核心程式碼如下:

@interface JDAppTestCase : NSObject
- (NSArray<NSString *> *)appTestMethods;
- (NSArray<NSString *> *)subClassNames;
@end

#import <objc/runtime.h>

@implementation JDAppTestCase
// 獲取所有測試方法
- (NSArray<NSString *> *)appTestMethods {
    NSString *str = [self performSelector:@selector(_shortMethodDescription)];
    NSArray *components = [str componentsSeparatedByString:@"\n\t\t"];
    NSMutableArray *testMethods = [NSMutableArray new];
    for (NSString *component in components) {
        if ([component containsString:@"test"] && ![component containsString:@"appTestMethods"]) {
            NSRange bRange = [component rangeOfString:@"test"];
            NSRange eRange = [component rangeOfString:@";"];
            NSString *method = [component substringWithRange:NSMakeRange(bRange.location, eRange.location - bRange.location)];
            [testMethods addObject:method];
        }
    }
    return testMethods;
}
// 獲取所有子類
- (NSArray<NSString *> *)subClassNames {
    int numClasses;
    Class *classes = NULL;
    numClasses = objc_getClassList(NULL,0);
    
    NSMutableArray *subClassNames = [NSMutableArray new];
    if (numClasses >0 ) {
        classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
        numClasses = objc_getClassList(classes, numClasses);
        for (int i = 0; i < numClasses; i++) {
            if (class_getSuperclass(classes[i]) == [self class]){
                [subClassNames addObject:NSStringFromClass(classes[i])];
            }
        }
        free(classes);
    }
    return subClassNames;
}
@end
複製程式碼

完善 App 的顯示

建立2個 TableViewControler,用於顯示測試方法。

JDAppTestsViewController

顯示所有 JDAppTestCase 子類。 點選名稱後,會顯示該類的所有測試方法。

JDTestMethodsViewController

顯示某個 JDAppTestCase 子類的所有 test 開頭的方法。 點選方法名後,因為 objc 呼叫方法,是通過發訊息實現的,所以可以很方便地藉助 performSelector 呼叫方法。

核心程式碼

JDAppTestsViewController

@implementation JDAppTestsViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"appTestsCellID" forIndexPath:indexPath];
    cell.textLabel.text = self.appTestsNames[indexPath.row];
    return cell;
}

#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *name = self.appTestsNames[indexPath.row];
    // 跳轉
    JDTestMethodsViewController *vc = [[JDTestMethodsViewController alloc] initWithAppTestsName:name];
    [self showViewController:vc sender:nil];
}

#pragma mark - Getters and setters
- (NSArray *)appTestsNames {
    if (!_appTestsNames) {
        JDAppTestCase *appTests = [JDAppTestCase new];
        _appTestsNames = appTests.subClassNames;
    }
    return _appTestsNames;
}

@end
複製程式碼

JDTestMethodsViewController

@implementation JDTestMethodsViewController
- (instancetype)initWithAppTestsName:(NSString *)name {
    self = [super init];
    if (self) {
        // 根據名稱獲得 JDAppTests 子類例項
        Class class = NSClassFromString(name);
        self.appTests = [class new];
    }
    return self;
}

#pragma mark - Table view delegate
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"testsMethodsCellID" forIndexPath:indexPath];
    cell.textLabel.text = self.testsMethodNames[indexPath.row];
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *name = self.testsMethodNames[indexPath.row];
    SEL selector = NSSelectorFromString(name);
    // 呼叫方法
    if ([self.appTests respondsToSelector:selector]) {
        [self.appTests performSelector:selector];
    }
}

#pragma mark - Getters and setters
- (NSArray *)testsMethodNames {
    if (!_testsMethodNames) {
        _testsMethodNames = [self.appTests appTestMethods];
    }
    return _testsMethodNames;
}
@end
複製程式碼

如何使用?

新增 NetworkRequestAppTests 繼承 JDAppTestCase。

新增類似於 XCTests 的程式碼,這些程式碼都不用在標頭檔案宣告,直接在 .m 檔案裡新增即可。

- (void)testConfigure {
    NSLog(@"configure result %d", [self.networkRequest configure]);
}

- (void)testLogin {
    [self.networkRequest loginWithCompletionHandler:^(BOOL success) {
        NSLog(@"login result %d", success);
    }];
}
複製程式碼

執行 App 工程,檢視具體效果

AppTests演示.gif

總結

前期搭建好 App Target,後期使用時,基本就是新增測試程式碼,使用方便,因為是在 App 上執行,可以配合著做效能測試。

不過每次都這麼搭建,還是很煩的,所以筆者做了個簡單的 Xcode 模板,將搭建過程自動化。

參考

獲取類的所有子類

獲取類所有方法的私有介面

相關文章