美文网首页程序员
iOS Framework 单元测试(二)-- JDAppTes

iOS Framework 单元测试(二)-- JDAppTes

作者: JiandanDream | 来源:发表于2018-04-18 22:51 被阅读117次

写在前面

笔者在使用了 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 模板,将搭建过程自动化。

参考

获取类的所有子类
获取类所有方法的私有接口

相关文章

网友评论

    本文标题:iOS Framework 单元测试(二)-- JDAppTes

    本文链接:https://www.haomeiwen.com/subject/tpabkftx.html