美文网首页
iOS 单元测试 - XCTest

iOS 单元测试 - XCTest

作者: z4ywzrq | 来源:发表于2020-06-22 23:26 被阅读0次

    原文链接:http://www.yupeng.fun/2020/05/18/xctest/

    简介

    单元测试(Unit Testing)又称为模块测试,是针对程序模块软件设计来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。

    单元测试通常由软件开发人员编写,用于确保他们所写的代码符合软件需求和遵循开发目标。通常来说,每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到工作目标要求。

    Xcode 集成了对单元测试的支持 XCTest。XCTest 是从 Xcode5 开始引入的一个测试框架,是上一代测试框架 OCUnit 的更现代化实现。XCTest 提供了与 Xcode 更好的集成。下面我们简单介绍下XCTest的使用。

    XCTest

    在 Xcode 新建项目时,勾选 Unit Tests 和 UI Tests,会创建对应的测试 target,并创建了继承于XCTestCase 的测试用例类,该类继承自 XCTestCase 类,其中包含三个方法:setUp,tearDown和 testExample。

    • setUp 用于在测试前设置好需要用到的对象等
    • tearDown 在测试结束时调用
    • testExample 是一个测试方法,测试方法命名通常是 testXXX 的格式,且不能有参数,不然不会识别为测试方法,测试方法的执行顺序是按照方法名中 test 后面的字符顺序执行的。
    • measureBlock: 性能测试方法,将需要性能测试的代码放入 block 里,运行这个方法会执行多次,运行时间比对设定的标准值和偏差判断是否可以通过测试

    创建完成后,就可以在测试方法里,编写测试代码,然后点击方法前的菱形按钮运行测试方法, 也可以使用快捷键 command+u 运行整个测试单元。正确运行后显示绿色对勾,运行错误会显示红色叉号。

    断言

    大部分的测试方法使用断言决定的测试结果。所有断言都有一个类似的形式:比较,表达式为真假,强行失败等。

    XCTFail(format...)  直接Fail
    XCTAssertNil(a1,format...)为空判断, a1为空时通过,反之不通过;
    XCTAssertNotNil(a1,format...) 不为空判断,a1不为空时通过,反之不通过;
    XCTAssert(expression,format...) 当expression求值为true时通过;
    XCTAssertTrue(expression,format...) 当expression求值为true时通过;
    XCTAssertFalse(expression,format...) 当expression求值为False时通过;
    XCTAssertEqualObjects(a1, a2,format...) 判断相等 [a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
    XCTAssertNotEqualObjects(a1, a2,format...) 判断不等,[a1 isEqual:a2]值为False时通过;
    XCTAssertEqual(a1, a2, format...)判断相等(当a1和a2是标量、结构体或联合体时使用,实际测试发现NSString也可以);
    XCTAssertNotEqual(a1, a2, format...)判断不等(当a1和a2是标量、结构体或联合体时使用);
    XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
    XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
    XCTAssertThrows(expression, format...)异常测试,当expression发生异常时通过;反之不通过;(很变态)
    XCTAssertThrowsSpecific(expression, specificException, format...) 异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过;
    XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
    XCTAssertNoThrow(expression, format…)异常测试,当expression没有发生异常时通过测试;
    XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
    

    自定义断言宏
    在使用断言时,经常使用一些特定情况的断言,写非常的啰嗦,难以阅读。并且还都是重复代码。可以通过编写自己的断言宏来解决这个问题。例如:

    NSString *string = @"http";
    XCTAssertTrue([string isKindOfClass:[NSString class]] && [string hasPrefix:@"http"],
        @"'%@' is not a valid URL string", string);
    
    
    //自定义断言
    #define AssertIsValidURLString(a) \
    if (![a isKindOfClass:[NSString class]] || ![a hasPrefix:@"http"]) { \
        XCTFail(@"'%@' is not a valid URL string", a); \
    }\
    
    NSString *text = @"123";
    AssertIsValidURLString(text);
    

    对于更复杂的断言和检查,可以使用简单的辅助类,方便检查。

    异步测试

    测试异步方法时,例如网络请求等耗时操作,由于执行结果不是立即就能获取到,XCTest 提供了一些辅助方法,如下例所示:

    - (void)testAsynExample {
        XCTestExpectation *expectation = [self expectationWithDescription:@"操作超时。。"];
        NSOperationQueue *queue = [[NSOperationQueue alloc]init];
        [queue addOperationWithBlock:^{
            sleep(2); //模拟耗时操作
            [expectation fulfill];
            XCTAssert(YES, @"fail"); //判断异步方法的结果是否正确
        }];
    
        //等待 XCTestExpectation fulfill,设置延时等待多少秒,如果超时就报错
        [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) {
            if (error) {
                NSLog(@"Error: %@", error);
            }
        }];
    }
    

    waitForExpectationsWithTimeout: 方法会在规定时间内,等待期望 XCTestExpectation 满足 fulfill,规定时间内不满足期望就会报错。

    异步测试除了使用 expectationWithDescription 以外,还可以使用 expectationForPredicate 和 expectationForNotification

    • expectationForPredicate
    - (void)testAsynExample {
        XCTAssertNil(self.imageView.image);
        [self.imageView setImageWithURL:self.jpegURL];
        
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"image != nil"];
        [self expectationForPredicate:predicate evaluatedWithObject:self.imageView handler:nil];
        [self waitForExpectationsWithTimeout:10 handler:nil];
    }
    

    NSPredicate 谓词判断,是否加载出了图片,self.imageView.image != nil,在规定时间内是否测试通过。

    • expectationForNotification 监听一个通知,在规定时间内等待,是否收到通知
    - (void)testAsynExample {
        //....
        [self expectationForNotification:@"NotificationName" object:nil handler:nil];
        [self waitForExpectationsWithTimeout:10 handler:nil];
    }
    

    UITest

    上面介绍的单元测试是对 app 的业务逻辑以及网络接口方面的测试。下面来介绍一下 UI 的测试。 在创建项目时勾选 UI Tests 会创建对应的 UI 测试的 target,如果你要在已有项目中添加 UI Tests 的话,可以新建一个 iOS UI Testing 的 target。创建完成后和上面一样也会创建对应的继承于 XCTestCase 测试类。

    UI 行为录制

    写好 UI 后就可以,进行我们的 UI 测试了,在 setUp 中,我们使用 XCUIApplication 的 launch 方法来启动测试 app。XCUIApplication 是 UIApplication 在测试进程中的代理 (proxy),我们可以在 UI 测试中通过这个类型和应用本身进行一些交互,比如开始或者终止一个 app。
    然后使用 Xcode 的 UI Testing 直接录制操作,操作如下:

    点击录制按钮,启动 app,点击 UI 就会在测试方法中,生成对应的测试代码,看起来很厉害的样子。

    获取 UI 元素

    在录制时,点击输入框,可以看到获取 UI 元素的代码,如下:

    - (void)testExample {
        XCUIApplication *app = [[XCUIApplication alloc] init];
        XCUIElement *element = [[[[[[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element;
        [[element childrenMatchingType:XCUIElementTypeTextField].element tap];
        [[element childrenMatchingType:XCUIElementTypeSecureTextField].element tap];
        
        [app.buttons[@"login"].staticTexts[@"login"] tap];
    }
    

    自动录制生成的代码使用了很多 query 来查询文本框,获取代表 app 中具体 UI 元素的 XCUIElement,然后对其进行测试操作。但是这样产生大量代码,难以理解,我们可使用简洁的方法获取 UI 元素。
    在 Interface Builder 或者代码中进行设置 textfield 的 identifier :

    - (void)testExample {
        
        NSString *name = @"admin";
        NSString *pwd = @"123";
        
        XCUIApplication *app = [[XCUIApplication alloc] init];
        //获取 name 输入框
        XCUIElement *nameTextField = app.textFields[@"nameTextField"];
        [nameTextField tap];
        [nameTextField typeText:name]; //输入框中写入文字
        
        //获取 pwd 输入框
        XCUIElement *pwdTextField = app.secureTextFields[@"pwdTextField"];
        [pwdTextField tap];
        [pwdTextField typeText:pwd];
        
        //点击 login 按钮
        [app.buttons.staticTexts[@"login"] tap];
        
        //登录需要网络请求,等待一段时间。登录成功 push 到下一个页面
        //这里判断在规定的时间内导航栏是否 push 过去
        XCUIElement *nav = app.navigationBars[name].staticTexts[name];
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"exists == 1"];
        [self expectationForPredicate:predicate evaluatedWithObject:nav handler:nil];
        [self waitForExpectationsWithTimeout:6 handler:nil];
    }
    

    上面的操作是获取两个输入框,并写入内容,点击登录 push 到下一个页面。

    总结

    本篇文章介绍了,使用 Xcode 来进行单元测试的一些操作,可以看到还是很方便快捷的。熟练掌握单元测试的一些技巧,对于提高 app 的质量还是有很大帮助的。

    References

    iOS单元测试
    XCTest 测试实战
    WWDC15 Session笔记 - Xcode 7 UI 测试初窥

    相关文章

      网友评论

          本文标题:iOS 单元测试 - XCTest

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