前言 不写单元测试的程序员是不合格的,为了让自己成为一名合格的程序员,学习如何写单元测试是很有必要的,这里以Xcode集成的测试框架XCTest 为例。本文首先会介绍XCTest单元测试的基础用法,然后结合具体的实例分析,最后动手写一个单元测试。
XCTest 基础用法 默认的测试类继承自XCTestCase
,当然也可以自定义测试类,添加一些公共的辅助方法。例如AFNetworking
的所有测试用例类都有一个共同的父类AFTestCase
,它是XCTestCase的子类,AFNetworking
所有测试类都是AFTestCase类的子类,这块在后面会具体讲到。需要额外注意的是所有的测试方法都必须以test
开头,且不能有参数,不然不会识别为测试方法,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @interface DemoUnitTestsTests : XCTestCase@end @implementation DemoUnitTestsTests- (void)setUp { [super setUp] ; } - (void)tearDown { [super tearDown] ; } - (void)testExample { } - (void)testPerformanceExample { [self measureBlock:^{ // Put the code you want to measure the time of here. }] ; } @end
断言 XCTest的断言具体可查阅XCTestAssertions.h
文件,这里还是做个简单的总结
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 XCTFail (format…) XCTAssertNil (a1, format...) XCTAssertNotNil (a1, format…) XCTAssert (expression, format...) XCTAssertTrue (expression, format...) XCTAssertFalse (expression, format...) XCTAssertEqualObjects (a1, a2, format...) XCTAssertNotEqualObjects (a1, a2, format...) XCTAssertEqual (a1, a2, format...) XCTAssertNotEqual (a1, a2, format...) XCTAssertEqualWithAccuracy (a1, a2, accuracy, format...) XCTAssertNotEqualWithAccuracy (a1, a2, accuracy, format...) XCTAssertThrows (expression, format...) XCTAssertThrowsSpecific (expression, specificException, format...) XCTAssertThrowsSpecificNamed (expression, specificException, exception_name, format...) XCTAssertNoThrow (expression, format…) XCTAssertNoThrowSpecific (expression, specificException, format...) XCTAssertNoThrowSpecificNamed (expression, specificException, exception_name, format...)
当然在有些特殊情况下直接使用这些断言,会让代码看起来很臃肿,比如:
1 XCTAssertTrue ([string isKindOfClass:[NSString class] ] && ([[NSUUID alloc] initWithUUIDString :string] != nil), @"'%@' is not a valid UUID string" , string) ;
我们可以自定义断言宏来解决这个问题:
1 #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)
即可,更多的封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #def ine assertTrue (expr) XCTAssertTrue ((expr) , @"" )#def ine assertFalse (expr) XCTAssertFalse ((expr) , @"" )#def ine assertNil (a1) XCTAssertNil ((a1) , @"" )#def ine assertNotNil (a1) XCTAssertNotNil ((a1) , @"" )#def ine assertEqual (a1, a2) XCTAssertEqual ((a1) , (a2), @"" )#def ine assertEqualObjects (a1, a2) XCTAssertEqualObjects ((a1) , (a2), @"" )#def ine assertNotEqual (a1, a2) XCTAssertNotEqual ((a1) , (a2), @"" )#def ine assertNotEqualObjects (a1, a2) XCTAssertNotEqualObjects ((a1) , (a2), @"" )#def ine assertAccuracy (a1, a2, acc) XCTAssertEqualWithAccuracy ((a1) ,(a2),(acc))
期望 期望实际上是异步测试,当测试异步方法时,因为结果并不是立刻获得,所以我们可以设置一个期望,期望是有时间限定的的,fulfill表示满足期望。 例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 - (void )testAsynExample { XCTestExpectation *exp = [self expectationWithDescription:@"这里可以是操作出错的原因描述。。。" ]; NSOperationQueue *queue = [[NSOperationQueue alloc]init]; [queue addOperationWithBlock:^{ sleep(2 ); XCTAssertEqual(@"a" , @"a" ); [exp fulfill]; }]; [self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) { if (error) { NSLog(@"Timeout Error: %@" , error); } }]; }
异步测试除了使用 expectationWithDescription
以外,还可以使用 expectationForPredicate
和expectationForNotification
,具体的可以看看这里 。
实例分析 这里以AFNetworking
为例,前面提到了AFNetworking
的所有测试用例类都有一个共同的父类AFTestCase
,它也是XCTestCase
的子类。在这个类中,添加了一些熟悉和公共方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #import <XCTest/XCTest.h> 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - (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 )) { 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
方法,那么就说明所有的测试期望在此期间都已经被实现。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - (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。
大部分网络请求都是异步操作,但是我们需要在主线程中获取到网络请求成功还是失败的信息。由于测试方法主线程执行完就会结束,所以需要设置一下,查看异步返回结果。这里我们使用期望在方法结束前设置等待,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 -(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, @"请求出错" ); }]; [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { if (error) { NSLog (@"Timeout Error: %@" , error); } }]; }
相关的Demo在这里 。
Reference XCTest测试实战
iOS单元测试
iOS单元测试(作用及入门提升)