原文链接: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 测试初窥