前言
不写单元测试的程序员是不合格的,为了让自己成为一名合格的程序员,学习如何写单元测试是很有必要的,这里以Xcode集成的测试框架XCTest为例。本文首先会介绍XCTest单元测试的基础用法,然后结合具体的实例分析,最后动手写一个单元测试。
XCTest
基础用法
默认的测试类继承自XCTestCase
,当然也可以自定义测试类,添加一些公共的辅助方法。例如AFNetworking
的所有测试用例类都有一个共同的父类AFTestCase
,它是XCTestCase的子类,AFNetworking
所有测试类都是AFTestCase类的子类,这块在后面会具体讲到。需要额外注意的是所有的测试方法都必须以test
开头,且不能有参数,不然不会识别为测试方法,具体如下:
@interface DemoUnitTestsTests : XCTestCase
@end
@implementation DemoUnitTestsTests
// 在每一个测试用例开始前调用,用来初始化相关数据
- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}
// 在测试用例完成后调用,可以用来释放变量等结尾操作
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}
// 测试方法
- (void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
// 性能测试方法,通过测试block中方法执行的时间,比对设定的标准值和偏差觉得是否可以通过测试
- (void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
@end
断言
XCTest的断言具体可查阅XCTestAssertions.h
文件,这里还是做个简单的总结
//通用断言
XCTFail(format…)
//为空判断,a1为空时通过,反之不通过;
XCTAssertNil(a1, format...)
//不为空判断,a1不为空时通过,反之不通过;
XCTAssertNotNil(a1, format…)
//当expression求值为TRUE时通过;
XCTAssert(expression, format...)
//当expression求值为TRUE时通过;
XCTAssertTrue(expression, format...)
//当expression求值为False时通过;
XCTAssertFalse(expression, format...)
//判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
XCTAssertEqualObjects(a1, a2, format...)
//判断不等,[a1 isEqual:a2]值为False时通过;
XCTAssertNotEqualObjects(a1, a2, format...)
//判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);
XCTAssertEqual(a1, a2, format...)
//判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
XCTAssertNotEqual(a1, a2, format...)
//判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)
//判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...)
//异常测试,当expression发生异常时通过,反之不通过;
XCTAssertThrows(expression, format...)
//异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过
XCTAssertThrowsSpecific(expression, specificException, format...)
//异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)
//异常测试,当expression没有发生异常时通过测试;
XCTAssertNoThrow(expression, format…)
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertNoThrowSpecific(expression, specificException, format...)
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)
当然在有些特殊情况下直接使用这些断言,会让代码看起来很臃肿,比如:
XCTAssertTrue([string isKindOfClass:[NSString class]] && ([[NSUUID alloc] initWithUUIDString:string] != nil), @"'%@' is not a valid UUID string", string);
我们可以自定义断言宏来解决这个问题:
#define AssertIsValidUUIDString(a1) \ do { \ NSUUID *_u = ([a1 isKindOfClass:[NSString class]] ? [[NSUUID alloc] initWithUUIDString:(a1)] : nil); \ if (_u == nil) { \ XCTFail(@"'%@' is not a valid UUID string", a1); \ } \ } while (0)
使用时只需要调用AssertIsValidUUIDString(string)
即可,更多的封装:
#define assertTrue(expr) XCTAssertTrue((expr), @"")
#define assertFalse(expr) XCTAssertFalse((expr), @"")
#define assertNil(a1) XCTAssertNil((a1), @"")
#define assertNotNil(a1) XCTAssertNotNil((a1), @"")
#define assertEqual(a1, a2) XCTAssertEqual((a1), (a2), @"")
#define assertEqualObjects(a1, a2) XCTAssertEqualObjects((a1), (a2), @"")
#define assertNotEqual(a1, a2) XCTAssertNotEqual((a1), (a2), @"")
#define assertNotEqualObjects(a1, a2) XCTAssertNotEqualObjects((a1), (a2), @"")
#define assertAccuracy(a1, a2, acc) XCTAssertEqualWithAccuracy((a1),(a2),(acc))
期望
期望实际上是异步测试,当测试异步方法时,因为结果并不是立刻获得,所以我们可以设置一个期望,期望是有时间限定的的,fulfill表示满足期望。
例如:
- (void)testAsynExample {
XCTestExpectation *exp = [self expectationWithDescription:@"这里可以是操作出错的原因描述。。。"];
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperationWithBlock:^{
//模拟这个异步操作需要2秒后才能获取结果,比如一个异步网络请求
sleep(2);
//模拟获取的异步操作后,获取结果,判断异步方法的结果是否正确
XCTAssertEqual(@"a", @"a");
//如果断言没问题,就调用fulfill宣布测试满足
[exp fulfill];
}];
//设置延迟多少秒后,如果没有满足测试条件就报错
[self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"Timeout Error: %@", error);
}
}];
}
异步测试除了使用 expectationWithDescription
以外,还可以使用 expectationForPredicate
和expectationForNotification
,具体的可以看看这里。
实例分析
这里以AFNetworking
为例,前面提到了AFNetworking
的所有测试用例类都有一个共同的父类AFTestCase
,它也是XCTestCase
的子类。在这个类中,添加了一些熟悉和公共方法:
#import
extern NSString * const AFNetworkingTestsBaseURLString;
@interface AFTestCase : XCTestCase
/**
* 默认 https://httpbin.org/ 一个http库测试工具
*/
@property (nonatomic, strong, readonly) NSURL *baseURL;
@property (nonatomic, assign) NSTimeInterval networkTimeout;
- (void)waitForExpectationsWithCommonTimeoutUsingHandler:(XCWaitCompletionHandler)handler;
@end
这里有两个属性,一个方法,baseURL
不用说是测试地址。networkTimeout
是网络请求超时时间,waitForExpectationsWithCommonTimeoutUsingHandler
是超时后的方法捕获回调,那么什么时候调用这个方法呢,举个例子:
在Xcode 6之前的版本里面并没有内置XCTest,想使用异步测试的只能是在主线程的RunLoop里面使用一个while循环,然后一直等待响应或者直到timeout:
- (void)testAsync {
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:5.0];
__block BOOL responseHasArrived = NO;
[self requestUrl:@"http://httpbin.com"
completionHandler:^(NSString *info) {
responseHasArrived = YES;
XCTAssert(info.length > 0);
}];
while (responseHasArrived == NO && ([timeoutDate timeIntervalSinceNow] > 0)) {
// 启动runloop,设置RunLoop最大时间(假无限循环),执行完毕是否退出
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, YES);
}
if (responseHasArrived == NO) {
XCTFail(@"Test timed out");
}
}
while循环在主线程里面每隔0.01秒会跑一次,直到有响应或者5秒之后超出响应时间限制才会跳出。
而使用XCTest的测试期望来实现这个,测试框架就会预计它在之后的某一时刻被实现。最终的程序完成代码块中的测试代码会调用XCTestExpection
类中的fulfill
方法来实现期望。这一方法替代了我们之前例子里面使用responseHasArrived
作为Flag的方式,这时我们让测试框架等待(有时限)测试期望通过XCTestCase的waitForExpectationsWithTimeout:handler:
方法实现。如果完成处理的代码在指定时限里执行并调用了fulfill
方法,那么就说明所有的测试期望在此期间都已经被实现。如:
- (void)testAsync {
XCTestExpectation *expectation =
[self expectationWithDescription:@"High Expectations"];
[self.pageLoader requestUrl:@"http://httpbin.com"
completionHandler:^(NSString *info) {
XCTAssert(info.length > 0);
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
if (error) {
NSLog(@"Timeout Error: %@", error);
}
}];
}
在最后的代码段里面使用[expectation fulfill]
来告知此次测试所期望的部分已经确切实现过了。然后用waitForExpectationsWithTimeout:handler
方法等待响应,这段会在接受响应之后执行或者超时之后也会执行。
实战
还是以AFNetworking
为例,写一个测试网络请求的测试用例,这里用cocoapods导入AFNetworking
,需要注意的是此时AFNetworking
在单元测试里无法使用,需要手动配置路径,步骤为:
- 1.复制Target(App) - Build Setting - Header Search Paths 的路径。
- 2.粘贴到Target(UnitTests) - Build Setting - Header - Search Paths里。
- 3.复制Target(App) - Build Setting - User-Defined - PODS_ROOT整条。
- 4.到Target(UnitTests) - Build Setting - User-Defined新建一条PODS_ROOT。
大部分网络请求都是异步操作,但是我们需要在主线程中获取到网络请求成功还是失败的信息。由于测试方法主线程执行完就会结束,所以需要设置一下,查看异步返回结果。这里我们使用期望在方法结束前设置等待,如下:
-(void)testRequest{
XCTestExpectation *expectation =[self expectationWithDescription:@"没有满足期望"];
AFHTTPSessionManager *sessionManager = [AFHTTPSessionManager manager];
sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
[sessionManager GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"responseObject:%@", [NSJSONSerialization JSONObjectWithData:responseObject options:1 error:nil]);
XCTAssertNotNil(responseObject, @"返回出错");
[expectation fulfill];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
XCTAssertNil(error, @"请求出错");
}];
// 设置5秒的超时时间
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
if (error) {
NSLog(@"Timeout Error: %@", error);
}
}];
}
相关的Demo在这里。
博客地址
Reference
XCTest测试实战
iOS单元测试
iOS单元测试(作用及入门提升)