iOS 单元测试&UI测试(1)

学习重点

单元&UI测试的意义

单元&UI测试执行流程探究及优化

单元&UI测试原理初探

1. 单元&UI测试

1.1 什么是单元测试

  单元测试是检查每个代码单元(例如类或函数)是否能产生预期的结果,单元测试是独立运行的,不依赖于其他模块或组件。

1.2 什么是UI测试

  UI测试是属于端到端的测试,是从应用程序启动到结束的测试过程,完全按照用户与应用程序交互的方式来复制与应用程序的交互,比单元测试慢的多,运行起来也更消耗资源。

1.3 需要进行测试的内容

 &emsp测试应涵盖以下的内容:

  • 核心功能:模型类和方法及其与控制器的交互

  • UI工作流程

  • 特殊的边界条件

  • Bug处理

1.4 测试原则(FIRST)

  • Fast:测试模块应该是快速高效的

  • Independent/Isolated:测试模块应该是独立、相互不影响的

  • Repeatable:测试实例应该是可以重复使用的,测试结果应该是相同的

  • Self-validating:测试应完全自动化。输出结果要么是“成功”,要么是“失败”

  • Timely:理想情况下,应该在编写要测试的生产代码之前编写测试(测试驱动开发)

2. 单元&UI测试执行流程探究

  首先,使用Xcode创建一个iOS工程,勾选上Include Tests选项,如下图所示:

image.png

  在测试代码运行之前,先来抛出几个问题?

    1. 测试代码运行之前需不需要启动APP
    1. 需不需要调用AppDelegate中的didFinishLaunchingWithOptions方法?

  为了验证这两个问题的确切答案,在测试工程打上如下的几个断点,如下图所示:

image.png

image.png

  接着运行测试工程中TestAppDemoTests.m文件中的testExample方法,如下图所示:

image.png

  首先,可以看到,App在模拟器中被启动了,并且程序执行到了main.m文件中设置的断点处,如下图所示:

image.png

  过掉断点,程序执行到了AppDelegate.m文件中设置的断点处,如下图所示:

image.png

  过掉断点,打印了在测试方法中输出的日志信息,如下图所示:

image.png

  其中:

  • 红框1:中的时间表示的是测试方法执行的时间
  • 红框2:表示的是测试方法的名字
  • 红框3:表示测试通过并且耗时0.001秒。

  根据以上的运行结果,可以很清楚的看到程序执行测试方法的时候是会启动APP并且会调用执行didFinishLaunchingWithOptions方法的,在一些大型项目中,通常会在didFinishLaunchingWithOptions方法中执行一些耗时的方法,那么这样就不能快速进行测试了,为了解决这个问题,可以选择创建一个FakeAppDelegate,只要在测试的时候在main函数中返回这个FakeAppDelegate对象就可以了,代码如下所示:

//FakeAppDelegate.h文件中代码

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface FakeAppDelegate : UIResponder <UIApplicationDelegate>
@end

NS_ASSUME_NONNULL_END



//FakeAppDelegate.m文件中代码

#import "FakeAppDelegate.h"

@interface FakeAppDelegate ()

@end

@implementation FakeAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions {
    
    
    return YES;
}

#pragma mark - UISceneSession lifecycle

- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
    
    return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}

- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
}

@end


//main.m文件中代码

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "FakeAppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        BOOL isShouldReturnFakeAppDelegate = NO;
        
        appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
复制代码

  你也许会注意到isShouldReturnFakeAppDelegate这个局部变量的值为NO,这样的话不就没什么作用了吗?这也正是我想抛出的问题,应该如何设置isShouldReturnFakeAppDelegate的值呢?在运行的过程中,如何知道此时是测试方法的调用还是实际APP的运行呢?读者不妨来思考一下这个问题,我会在接下来的讨论中分享两种解决方案。

3. 单元&UI测试执行流程优化

3.1 OC工程执行流程优化

3.1.1 使用runtime API进行判断

  首先你应该注意到的是测试工程中类的继承顺序为:TestAppDemoTests-->XCTestCase-->XCTest,那么可以从这里入手,判断XCTest这个类是否存在就可以了,在main.m文件中编写如下的代码:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "FakeAppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        BOOL isShouldReturnFakeAppDelegate = NSClassFromString(@"XCTest") != nil;
        
        appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
复制代码

  然后分别在AppDelegate.m文件以及FakeAppDelegate.m文件中打印如下日志信息:

image.png

image.png

  command+r运行App,输出日志信息如下图所示:

image.png

  运行TestAppDemoTests.m文件中的testExample方法,输出日志信息如下图所示:

image.png

3.1.2 使用环境变量进行判断

  在工程Scheme下的Test中的Debug模式下添加环境变量IS_TESTING,如下图所示:

image.png

image.png

  然后在main.m文件中编写如下代码,获取环境变量IS_TESTING的值

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import "FakeAppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        BOOL isShouldReturnFakeAppDelegate = [[NSProcessInfo processInfo].environment[@"IS_TESTING"] boolValue];;
        
        appDelegateClassName = NSStringFromClass(isShouldReturnFakeAppDelegate ? [FakeAppDelegate class] : [AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
复制代码

  command+r运行App,输出日志信息如下图所示:

image.png

  运行TestAppDemoTests.m文件中的testExample方法,输出日志信息如下图所示:

image.png

3.2 Swift工程执行流程优化

  • 创建一个简单的Swift工程TestSwiftDemo,如下图所示:

image.png

  • 会发现在Swift工程中并没有main.swift文件

image.png

  • 创建main.swift文件并在这个文件中编写如下代码:
import UIKit


var appDelegateClsName = NSStringFromClass(AppDelegate.self)

//两种判断方式任选其一

//1.根据XCTest是否存在判断是否正在执行测试方法
if NSClassFromString("XCTest") != nil  {
    
    appDelegateClsName = NSStringFromClass(FakeAppDelegate.self)
}

//2.根据环境变量判断是否正在执行测试方法
if ProcessInfo.processInfo.environment["IS_TESTING"] == "true" {
    appDelegateClsName = NSStringFromClass(FakeAppDelegate.self)
}

let argv = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<CChar>.self, capacity: Int(CommandLine.argc))

_ = UIApplicationMain(CommandLine.argc, argv, nil, appDelegateClsName)
复制代码
  • Test中配置如下图所示环境变量

image.png

  • 运行结果如下图所示:

image.png

4. 单元&UI测试原理初探

  在使用command + u运行测试工程之后,按照如下图所示的方式打开编译之后产生的应用程序,会发现会多出AutoTestingDemoUITests-Runner这个应用程序。

image.png

  而模拟器中也会安装这个应用程序,如下图所示:

image.png

  进入到AutoTestingDemo这个APP包中,查看其Frameworks中的文件,发现多了以下几个文件:

image.png

image.png

  也就是说,是不是在一个ipa包中只要包含了这几个动态库,就可以在项目中运行测试代码了呢?而XCTest.framework又是从哪里拷贝进来的呢?其实是从Xcode中的以下路径中获取的:

  • XCTest.framework(模拟器设备):/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framework

  • XCTest.framework(真机设备):/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework

  只要在工程中添加了XCTest.framework,就可以不创建测试target也能在工程中进行代码测试了,首先创建一个xcconfig文件,如下图所示:

image.png

  接着在这个xcconfig文件中按照如下的方式进行配置:

//1.设置动态库头文件路径
HEADER_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks/XCTest.framework/Headers"

//2.链接动态库

//2.1传统方式
OTHER_LDFLAGS = $(inherited) -F "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks" -framework "XCTest"

//2.2 

//3.配置rpath 解决 崩溃Reason: image not found
LD_RUNPATH_SEARCH_PATHS = $(inherited) "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Frameworks"
复制代码

  然后主工程引用这个xcconfig文件,如下图所示:

image.png

 &emsp创建一个AppTests类,其代码如下所示:

//AppTests.h文件中的代码

#import <XCTest/XCTest.h>

NS_ASSUME_NONNULL_BEGIN

@interface AppTests : XCTestCase

- (void)testExample1;

- (void)testExample2;

@end

NS_ASSUME_NONNULL_END



//AppTests.m文件中的代码

#import "AppTests.h"

@implementation AppTests

- (void)testExample1 {
    NSLog(@"-------testExample1-------");
}

- (void)testExample2 {
    NSLog(@"-------testExample2-------");
}

@end
复制代码

  然后在ViewController.m文件中编写如下所示的代码:

#import "ViewController.h"
#import <XCTest/XCTest.h>
#import "AppTests.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //管理者 XCTestSuite,用来管理测试用例
    XCTestSuite *suite = [XCTestSuite defaultTestSuite];
    //测试用例
    AppTests *testCase = [AppTests testCaseWithSelector:@selector(testExample1)];
    
    [suite addTest:testCase];
    //遍历其中所有的测试用例,调用其所有的测试方法
    for (XCTest *test in suite.tests) {
        [test runTest];
    }
    
}

@end
复制代码

  接着运行程序,然后点击屏幕,日志输出如下图所示:

image.png

  但以上代码有个问题,当再次点击屏幕的时候,应用程序会直接奔溃,如下图所示:

image.png

  如果想要多次运行测试用例,可以通过以下的方式进行调用

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //管理者 XCTestSuite,用来管理测试用例
    XCTestSuite *suite = [XCTestSuite testSuiteForTestCaseClass:AppTests.class];
    //测试用例
    AppTests *testCase = [AppTests new];
    
    [suite addTest:testCase];
    //遍历其中所有的测试用例,调用其所有的测试方法
    for (XCTest *test in suite.tests) {
        [test runTest];
    }
    
}
复制代码

  运行程序,点击屏幕,控制台输出信息如下图所示:

image.png

  未完待续...

猜你喜欢

转载自juejin.im/post/7019149438002135070