美文网首页
iOS开发之进阶篇(6)—— 单元测试(Unit Tests 和

iOS开发之进阶篇(6)—— 单元测试(Unit Tests 和

作者: 看影成痴 | 来源:发表于2020-06-18 19:05 被阅读0次

    版本

    Xcode 11.5

    目录

    1. 概念
    2. 准备工作
    3. Unit Tests
    4. UI Tests

    1. 概念

    1.1 单元测试

    单元测试是指对软件中的最小可测试单元进行检查和验证. Xcode中有两种单元测试 (Unit Tests 和 UI Tests), Unit Tests 用于测试功能模块; UI Tests用于测试UI交互.

    • Unit Tests 用于测试功能模块, 这些功能模块应尽量单一, 避免与其他功能耦合. 比如测试一个比大小的函数, 一个请求网络的功能等等.
    • UI Tests 用于UI交互. 它可以通过编写代码或者是记录开发者的手动操作过程并代码化, 来实现自动点击某个按钮、视图, 或者自动输入文字等功能.

    1.2 测试用例

    指我们用于测试某个功能或者UI交互的测试代码.

    1.3 断言

    断言主要作用是可以让开发者比较便捷的捕获一个错误, 让程序崩溃, 同时报出错误提示. 如果某个断言不通过, 程序将报错, 并定格在断言所在行.
    断言只在debug模式下起作用, 在release模式下将被忽略.
    一些常用的断言:

    XCTAssertNil(expression, ...) expression为空时通过, ...可填入报错信息
    XCTAssertNotNil(expression, ...)  expression不为空时通过
    XCTAssert(expression, ...)  expression为true时通过
    XCTAssertTrue(expression, ...)  expression为true时通过
    XCTAssertFalse(expression, ...)  expression为false时通过
    XCTAssertEqual(expression1, expression2, ...)  expression1 = expression2 时通过
    XCTAssertEqualObjects(expression1, expression2, ...)  expression1 = expression2 时通过
    XCTAssertNotEqualObjects(expression1, expression2, ...)  expression1 != expression2 时通过
    

    2. 准备工作

    新建工程, 并勾选 Include Unit Tests 和 Include UI Tests, 然后系统将会自动创建单元测试target及模板代码.

    create.png

    如果新建工程时没有勾选那两项单元测试, 我们也可以后期添加之. 点击TARGET添加按钮:

    create2.png

    然后找到那两个单元测试:

    create3.png

    添加后就可看到测试代码块:

    tests.png

    这里Unit Tests的代码块文件夹名为工程名+Tests; 而UI Tests的代码块文件夹名为工程名+UITests.
    并且每个文件夹下都各自默认创建了一个测试类plist文件, 测试类只有.m文件而没有.h文件, 因为单元测试不需要外部来调用, 我们所有的测试工作都在.m文件里面完成.
    一个测试类 (.m文件) 里面可以写很多个测试用例. 但如果测试用例太多, 我们可以创建多个测试类以便于分类管理这些测试用例. 比如有专门用于测试算法函数的, 有专门用于测试各种网络请求功能的, 有专门用于测试工具类各功能是否正常的等等.
    新建测试类:

    create4.png

    例如:

    ms.png

    3. Unit Tests

    测试类继承于XCTestCase, 并且系统一开始就给出了如下示例代码:

    - (void)setUp {
        // 测试用例开始前执行 (初始化)
    }
    
    - (void)tearDown {
        // 测试用例结束后执行 (清理工作)
    }
    
    - (void)testExample {
        // 测试用例
    }
    
    - (void)testPerformanceExample {
        // 性能测试
        [self measureBlock:^{
            // 性能测试对象 (代码段)
        }];
    }
    

    我们的测试用例必须以test开头, 这样才会被系统识别为测试用例. 测试用例方法前面有一个菱形小框, 点击这个将会执行该测试用例, 测试通过则打V, 不通过则打X. 我们也可以⌘U来测试当前类所有的用例.

    示例1
    测试一个简单的比大小函数, 看测试结果是否正确.

    #import "KKAlgorithm.h"
    
    // 最大值
    - (void)testMaxValue {
        
        int value1 = 5;
        int value2 = 10;
        int maxInt = [KKAlgorithm maxValueWithValue1:value1 value2:value2];
            
        XCTAssertEqual(maxInt, value2);
    //    XCTAssertEqual(maxInt, value1, @"返回最大值错误!");
    }
    

    maxInt=value2=10, 测试通过. 如果把value2改为value1, 则测试不通过, 程序报错并定格在XCTAssertEqual所在行.

    KKAlgorithm.h

    @interface KKAlgorithm : NSObject
    
    // 获取最大值
    + (int)maxValueWithValue1:(int)value1 value2:(int)value2;
    
    @end
    

    KKAlgorithm.m

    // 获取最大值
    + (int)maxValueWithValue1:(int)value1 value2:(int)value2 {
        
        return value1 > value2 ? value1 : value2;
    }
    

    示例2
    请求网络. 因为请求网络是异步的, 我们需要等到网络返回才会判断测试结果. 所以本例中会引入测试等待的方法.
    等待方法:

        XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
        XCTestExpectation *expectation1 = [[XCTestExpectation alloc] initWithDescription:@"请求网络1"];
        XCTestExpectation *expectation2 = [[XCTestExpectation alloc] initWithDescription:@"请求网络2"];
        // 异步模拟请求网络1
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [expectation1 fulfill];     // 满足期望
        });
        // 异步模拟请求网络2
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [expectation2 fulfill];     // 满足期望
        });
        // 等待10秒, 如果expectation1和expectation2都满足期望, 则继续往下执行
        [waiter waitForExpectations:@[expectation1, expectation2] timeout:10.0];
    

    XCTestExpectation测试期望, 相当于等待的条件.
    XCTWaiter用于发动等待, 可以设置等待多个测试期望. 有代理方法, 但这里没有使用到故不详解.

    请求百度首页数据的demo:

    #import "KKHttp.h"
    
    @interface KKTestsDemoTests : XCTestCase <KKHttpDelegate> {
        
        XCTestExpectation *_expectation;
    }
    
    // 网络测试
    - (void)testHttp {
        
        XCTWaiter *waiter = [[XCTWaiter alloc] initWithDelegate:self];
        _expectation = [[XCTestExpectation alloc] initWithDescription:@"请求百度首页数据"];
        
        // 发起网络请求
        KKHttp *http = [[KKHttp alloc] init];
        http.delegate = self;
        [http fetchBaidu];
        
        // 等待10秒, 如果expectation满足期望, 则继续往下执行
        [waiter waitForExpectations:@[_expectation] timeout:10.0];
    }
    
    // 代理回调: 网络返回
    - (void)http:(KKHttp *)http receiveData:(NSData *)data error:(NSError *)error {
        
        XCTAssertNotNil(data, @"网络无响应");
        NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"dataStr:%@", dataStr);  // XML数据, 这里没进行解析
        
        [_expectation fulfill];     // 结束等待
    }
    

    因为测试希望XCTestExpectation在代理回调中满足, 所以期望对象设为全局变量XCTestExpectation *_expectation;.

    KKHttp.h

    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @class KKHttp;
    @protocol KKHttpDelegate <NSObject>
    @optional
    - (void)http:(KKHttp *)http receiveData:(nullable NSData *)data error:(nullable NSError *)error;
    @end
    
    @interface KKHttp : NSObject
    
    @property (nonatomic, weak) id<KKHttpDelegate>  delegate;
    
    - (void)fetchBaidu;
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    KKHttp.m

    #import "KKHttp.h"
    
    @implementation KKHttp
    
    - (void)fetchBaidu {
        
        NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0];
        NSURLSession *session = [NSURLSession sharedSession];
        NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if ([self.delegate respondsToSelector:@selector(http:receiveData:error:)]) {
                [self.delegate http:self receiveData:data error:error];
            }
        }];
        [task resume];
    }
    
    @end
    

    示例3
    实例2的等待方法毕竟很麻烦, 如果测试用例多了, 会产生很多重复代码. 下面讨论用通知的方法来等待, 然后把通知做成宏的形式, 方便调用.
    宏前传:

    // 异步测试
    - (void)testAlbumAuthorization {
           
        // 异步任务
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"222");
            // 发送通知: 结束等待
            [[NSNotificationCenter defaultCenter] postNotificationName:@"KKExpectationNotification" object:nil];
        });
        
        // 等待
        [self expectationForNotification:@"KKExpectationNotification" object:nil handler:nil];
        [self waitForExpectationsWithTimeout:5.0 handler:nil];
        NSLog(@"111");
    }
    

    注意
    expectationForNotification:object:handler:和waitForExpectationsWithTimeout:handler:要比postNotificationName:object:先执行. 也就是说, 要保证先打印111再打印222, 不然达不到我们预期效果.

    我们把等待和结束等待写成宏的形式, 方便别的测试用例调用:

    #define WAIT \
        [self expectationForNotification:@"KKExpectationNotification" object:nil handler:nil]; \
        [self waitForExpectationsWithTimeout:5.0 handler:nil];
    
    #define NOTIFY \
        [[NSNotificationCenter defaultCenter] postNotificationName:@"KKExpectationNotification" object:nil];
    
    // 异步测试
    - (void)testAlbumAuthorization {
           
        // 异步任务
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"222");
            // 发送通知: 结束等待
            NOTIFY
        });
        
        // 等待
        WAIT
        NSLog(@"111");
    }
    

    ps
    有时候在测试对象中import别的对象的时候, 系统会提示找不到头文件:

    Nofound.png

    这时候我们可以在单元测试的TARGET-->Build settings-->Header Search Paths中添加需要的头文件. 例如:

    headers.png

    4. UI Tests

    先来看看我们想要达到的效果:

    run.gif

    建立两个VC: VC1和VC2. 在VC1里输入账号和密码然后点击登录, 跳转到VC2, 接着点击VC2的Back按钮返回到VC1. 如此循环一万次, 测试我们的登录API有无问题.

    在UI Tests的测试用例里, 把光标扔进测试用例代码区, 然后点击小红圈开始录制App界面.

    record.png

    我们每对App屏幕互动一次, 代码区就会自动生成对应代码:

    - (void)testExample {
        
        // 运行App
        XCUIApplication *app = [[XCUIApplication alloc] init];
        [app launch];
    
        
        XCUIApplication *app = [[XCUIApplication alloc] init];
        XCUIElement *element = [[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
        [[[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0] tap];
        
        XCUIElement *aKey = app/*@START_MENU_TOKEN@*/.keys[@"a"]/*[[".keyboards.keys[@\"a\"]",".keys[@\"a\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/;
        [aKey tap];
        [aKey tap];
        [aKey tap];
        [aKey tap];
        [[[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:1] tap];
        
        XCUIElement *bKey = app/*@START_MENU_TOKEN@*/.keys[@"b"]/*[[".keyboards.keys[@\"b\"]",".keys[@\"b\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/;
        [bKey tap];
        [bKey tap];
        [bKey tap];
        [bKey tap];
        [app/*@START_MENU_TOKEN@*/.staticTexts[@"\U767b\U5f55"]/*[[".buttons[@\"\\U767b\\U5f55\"].staticTexts[@\"\\U767b\\U5f55\"]",".staticTexts[@\"\\U767b\\U5f55\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/ tap];
        [app.buttons[@"Back"] tap];
        
    }
    

    当然这些代码是raw的, 我们需要稍微做些修改才行:

    - (void)testExample {
        
        // 运行App
        XCUIApplication *app = [[XCUIApplication alloc] init];
        [app launch];
    
        for (int i=0; i<10; i++) {
            
            // 界面中的元素 (控件)
            XCUIElement *element = [[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
            
            // 找到第一个输入框, 并点击
            [[[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0] tap];
            
            sleep(1);   // 等待键盘弹出
            
            // 点击键盘
            XCUIElement *aKey = app/*@START_MENU_TOKEN@*/.keys[@"a"]/*[[".keyboards.keys[@\"a\"]",".keys[@\"a\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/;
            [aKey tap];
            
            // 找到第二个输入框, 并点击
            [[[element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:1] tap];
            
            // 点击键盘
            XCUIElement *bKey = app/*@START_MENU_TOKEN@*/.keys[@"b"]/*[[".keyboards.keys[@\"b\"]",".keys[@\"b\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/;
            [bKey tap];
            
            // 找到登录按钮, 并点击
            [app.staticTexts[@"\U0000767b\U00005f55"] tap];
    //        [app.staticTexts[@"登录"] tap];
            
            sleep(2);   // 等待加载VC2
            
            // 这时候已经跳转到VC2了
            // 找到Back按钮, 并点击
            [app.buttons[@"Back"] tap];
        }
    }
    

    注意:

    1. 有些地方使用sleep等待界面加载出来, 不然在屏幕中找不到改控件会报错;
    2. 中文登录按钮被自动生成@"\U767b\U5f55", 我们可以手动加入四个0或者直接写成中文.

    相关文章

      网友评论

          本文标题:iOS开发之进阶篇(6)—— 单元测试(Unit Tests 和

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