寫在前面
筆者在使用了 XCTests 對 Framework 進行單元測試過程中,發現無法使用 XCTests 進行真機測試,而專案剛好涉及到必須真機測試的功能。
所以簡單地做了個小工具,對其進行補充。
基本思路
目標:使用真機進行單元測試。
簡單粗暴的方式,就是建立一個 App 工程,然後將 Framework 工程直接拖到 App 工程裡。
優點:
- Framework 中的所有程式碼及資源,App 都可以訪問。
- 然後只要在 App 工程裡編寫測試程式碼就行。
缺點:
- 工程會比較複雜,甚至有點混亂,他人接手時,總會一頭霧水。
- 編寫測試程式碼時,因為會有很多測試用例,而且會不斷增加,如果每次都去修改 UI,修改呼叫方法,會浪費許多寶貴時間。
所以針對以上2個缺點,筆者對這種方案,進行了簡單的優化。
優化後方案:
- 在 Framework 工程中,新增一個 App Target,使用它來進行真機測試。
- 藉助 TableView,通過 Cell 去呼叫每個測試用例。
- 新增測試用例時,不需要改動原來程式碼。
如何做到上述第3點?
筆者想了一種簡單的實現方案:
- 建立一個測試基類 JDAppTestCase,所有測試類都去繼承它。
- JDAppTestCase 提供獲取所有子類的方法,這樣新增測試類,就不用去修改原來程式碼。
- JDAppTestCase 提供獲取『測試方法』的方法,這樣新增測試方法,也不用去修改原來程式碼。
模仿 XCTests,把這種方案稱為 AppTests。
所以關鍵在於 JDAppTestCase 這2個方法,該如何實現。
具體實現
建立一個 App Target 的過程,不再贅述。
建立完,需要新增對原來 Framework 工程的依賴,如圖:
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 工程,檢視具體效果
總結
前期搭建好 App Target,後期使用時,基本就是新增測試程式碼,使用方便,因為是在 App 上執行,可以配合著做效能測試。
不過每次都這麼搭建,還是很煩的,所以筆者做了個簡單的 Xcode 模板,將搭建過程自動化。