写在前面
笔者在使用了 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 模板,将搭建过程自动化。
网友评论