iOS 单元测试 - XCTest

原文链接: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 运行整个测试单元。正确运行后显示绿色对勾,运行错误会显示红色叉号。

iOS 单元测试 - XCTest_第1张图片


断言

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

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 直接录制操作,操作如下:

iOS 单元测试 - XCTest_第2张图片

点击录制按钮,启动 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 :

iOS 单元测试 - XCTest_第3张图片
- (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)