iOS測(cè)試我分三個(gè)篇介紹UI 測(cè)試后,覆蓋率測(cè)試,Unit單元測(cè)試.
本文介紹下面幾個(gè)功能邏輯等UnitTest部分:
1.邏輯功能測(cè)試
2.同,異步功能方法測(cè)試 - [分析AFNetworking解釋]
3.單元測(cè)試之Mock使用簡(jiǎn)介
4.性能耗時(shí)測(cè)試
5.單例測(cè)試
6.編寫測(cè)試用例該注意要點(diǎn)
7.封裝測(cè)試庫(kù)
8.自動(dòng)化測(cè)試,Jenkins的安裝和使用
9.自動(dòng)化單元測(cè)試,可以看LeanCloud 工程師的李智維的自動(dòng)化單元測(cè)試的直播錄影
李智維的演示github李智維的演示github
一 : 邏輯功能測(cè)試
(1)直接測(cè)試文件簡(jiǎn)單測(cè)試一個(gè)字符串是否為nil, 并熟悉XCTAssert
//簡(jiǎn)單例子
- (void)testExample {
NSString *name = @"明星";
XCTAssertNotNil(name, @"btn should not be nil");//報(bào)錯(cuò)提示語(yǔ):@"btn should not be nil"
}
上面簡(jiǎn)單在testExample中通過(guò)XCTAssertNotNil測(cè)試一下name是否為nil,點(diǎn)擊方法左側(cè)棱形按鈕測(cè)試.
我們定位XCTAssertNotNil跳轉(zhuǎn)到聲明文件XCTestAssertions.h文件,包含很多判斷,全部是宏定義方式,下面是網(wǎng)友的中文解釋:
XCTFail(format…) 生成一個(gè)失敗的測(cè)試蚂维;
XCTAssertNil(a1, format...)為空判斷堂飞,a1為空時(shí)通過(guò),反之不通過(guò)莱衩;
XCTAssertNotNil(a1, format…)不為空判斷,a1不為空時(shí)通過(guò)娇澎,反之不通過(guò)笨蚁;
XCTAssert(expression, format...)當(dāng)expression求值為TRUE時(shí)通過(guò);
XCTAssertTrue(expression, format...)當(dāng)expression求值為TRUE時(shí)通過(guò)趟庄;
XCTAssertFalse(expression, format...)當(dāng)expression求值為False時(shí)通過(guò)括细;
XCTAssertEqualObjects(a1, a2, format...)判斷相等,[a1 isEqual:a2]值為TRUE時(shí)通過(guò)戚啥,其中一個(gè)不為空時(shí)奋单,不通過(guò);
XCTAssertNotEqualObjects(a1, a2, format...)判斷不等猫十,[a1 isEqual:a2]值為False時(shí)通過(guò)览濒;
XCTAssertEqual(a1, a2, format...)判斷相等(當(dāng)a1和a2是 C語(yǔ)言標(biāo)量、結(jié)構(gòu)體或聯(lián)合體時(shí)使用,實(shí)際測(cè)試發(fā)現(xiàn)NSString也可以)拖云;
XCTAssertNotEqual(a1, a2, format...)判斷不等(當(dāng)a1和a2是 C語(yǔ)言標(biāo)量贷笛、結(jié)構(gòu)體或聯(lián)合體時(shí)使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判斷相等宙项,(double或float類型)提供一個(gè)誤差范圍乏苦,當(dāng)在誤差范圍(+/-accuracy)以內(nèi)相等時(shí)通過(guò)測(cè)試;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判斷不等尤筐,(double或float類型)提供一個(gè)誤差范圍汇荐,當(dāng)在誤差范圍以內(nèi)不等時(shí)通過(guò)測(cè)試洞就;
XCTAssertThrows(expression, format...)異常測(cè)試,當(dāng)expression發(fā)生異常時(shí)通過(guò)拢驾;反之不通過(guò)奖磁;(很變態(tài)) XCTAssertThrowsSpecific(expression, specificException, format...) 異常測(cè)試,當(dāng)expression發(fā)生specificException異常時(shí)通過(guò)繁疤;反之發(fā)生其他異晨或不發(fā)生異常均不通過(guò);
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)異常測(cè)試稠腊,當(dāng)expression發(fā)生具體異常躁染、具體異常名稱的異常時(shí)通過(guò)測(cè)試,反之不通過(guò)架忌;
XCTAssertNoThrow(expression, format…)異常測(cè)試吞彤,當(dāng)expression沒(méi)有發(fā)生異常時(shí)通過(guò)測(cè)試;
XCTAssertNoThrowSpecific(expression, specificException, format...)異常測(cè)試叹放,當(dāng)expression沒(méi)有發(fā)生具體異常饰恕、具體異常名稱的異常時(shí)通過(guò)測(cè)試,反之不通過(guò)井仰;
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)異常測(cè)試埋嵌,當(dāng)expression沒(méi)有發(fā)生具體異常、具體異常名稱的異常時(shí)通過(guò)測(cè)試俱恶,反之不通過(guò)
特別注意下XCTAssertEqualObjects和XCTAssertEqual雹嗦。
XCTAssertEqualObjects(a1, a2, format...)的判斷條件是[a1 isEqual:a2]是否返回一個(gè)YES。
XCTAssertEqual(a1, a2, format...)的判斷條件是a1 == a2是否返回一個(gè)YES合是。
(2) 測(cè)試項(xiàng)目中文件中的某個(gè)方法 - 沒(méi)有返回值
<2.1>創(chuàng)建一個(gè)LoginViewController文件,并在頭文件中加上- (void)loginWithPhone:(NSString *)phone code:(NSString *)code方法:
#import "LoginViewController.h"
@interface LoginViewController ()
@end
@implementation LoginViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
//手機(jī)驗(yàn)證碼登錄
- (void)loginWithPhone:(NSString *)phone code:(NSString *)code {
NSMutableDictionary *dic = @{}.mutableCopy;
[dic setObject:phone forKey:@"phone"];
[dic setObject:code forKey:@"code"];
NSLog(@"%@",dic);
}
@end
<2.2>在Tests文件夾下創(chuàng)建一個(gè)測(cè)試文件LoginVCtrlTests,創(chuàng)建變量,并調(diào)用其loginWithPhone方法
#import <XCTest/XCTest.h>
#import "LoginViewController.h"
@interface LoginVCtrlTests : XCTestCase
@property(nonatomic,strong)LoginViewController *loginVC;
@end
@implementation LoginVCtrlTests
- (void)setUp {
[super setUp];
self.loginVC = [[LoginViewController alloc]init];
}
- (void)tearDown {
[super tearDown];
self.loginVC = nil;
}
- (void)testExample {
[self.loginVC loginWithPhone:nil code:@"3345"];
}
點(diǎn)擊按鈕測(cè)試testExample方法,運(yùn)行后發(fā)現(xiàn)報(bào)錯(cuò)如下,意思是可變字典setObject插入對(duì)象不能為nil:
caught "NSInvalidArgumentException",
"*** -[__NSDictionaryM setObject:forKey:]:
object cannot be nil (key: phone)"
顯然loginWithPhone方法對(duì)參數(shù)沒(méi)有判斷完整.所以實(shí)際測(cè)試中,可以填入各種類型數(shù)據(jù)來(lái)完善該方法比如null,nil,@"",等等
- (void)testExample {
[self.loginVC loginWithPhone:nil code:@"3345"];
[self.loginVC loginWithPhone:null code:nil];
[self.loginVC loginWithPhone:@"" code:null];
}
(3) 測(cè)試項(xiàng)目中文件中的某個(gè)方法 - 有返回值
在LoginViewController添加下面校驗(yàn)手機(jī)號(hào)合法性方法,返回一個(gè)bool值:
- (BOOL)checkPhoneStr:(NSString *)phone {
//判斷phone是否合法的代碼
//....
return YES;
}
然后在測(cè)試文件- (void)testExample 中再加上下面兩行代碼,判斷手機(jī)是否合法(返回值是否為true,不然報(bào)錯(cuò)),然后運(yùn)行測(cè)試, 結(jié)果報(bào)錯(cuò),如下圖所示:
(所以現(xiàn)在要做的是傳各種參數(shù)吧)
二 : 異步功能方法測(cè)試 ,通過(guò)分析AFNetworking框架描述
AFNetworking涉及多線程和異步等功能,所以拿來(lái)學(xué)習(xí),下載 AFNetworking 項(xiàng)目,打開項(xiàng)目后直接進(jìn)入Tests目錄下面:
找到AFImageDownloaderTests.m文件,copy前部分代碼如下:
#import "AFTestCase.h"
#import "AFImageDownloader.h"
@interface AFImageDownloaderTests : AFTestCase
@property (nonatomic, strong) NSURLRequest *pngRequest;
@property (nonatomic, strong) NSURLRequest *jpegRequest;
@property (nonatomic, strong) AFImageDownloader *downloader;
@end
@implementation AFImageDownloaderTests
- (void)setUp {
[super setUp];
self.downloader = [[AFImageDownloader alloc] init];
[[AFImageDownloader defaultURLCache] removeAllCachedResponses];
[[[AFImageDownloader defaultInstance] imageCache] removeAllImages];
self.pngRequest = [NSURLRequest requestWithURL:self.pngURL];
self.jpegRequest = [NSURLRequest requestWithURL:self.jpegURL];
}
- (void)tearDown {
[self.downloader.sessionManager invalidateSessionCancelingTasks:YES];
self.downloader = nil;
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
self.pngRequest = nil;
}
#pragma mark - Image Download
- (void)testThatImageDownloaderSingletonCanBeInitialized {
AFImageDownloader *downloader = [AFImageDownloader defaultInstance];
XCTAssertNotNil(downloader, @"Downloader should not be nil");
}
- (void)testThatImageDownloaderCanBeInitializedAndDeinitializedWithActiveDownloads {
[self.downloader downloadImageForURLRequest:self.pngRequest
success:nil
failure:nil];
self.downloader = nil;
XCTAssertNil(self.downloader, @"Downloader should be nil");
}
- (void)testThatImageDownloaderReturnsNilWithInvalidURL
{
NSMutableURLRequest *mutableURLRequest = [NSMutableURLRequest requestWithURL:self.pngURL];
[mutableURLRequest setURL:nil];
/** NSURLRequest nor NSMutableURLRequest can be initialized with a nil URL,
* but NSMutableURLRequest can have its URL set to nil
**/
NSURLRequest *invalidRequest = [mutableURLRequest copy];
XCTestExpectation *expectation = [self expectationWithDescription:@"Request should fail"];
AFImageDownloadReceipt *downloadReceipt = [self.downloader
downloadImageForURLRequest:invalidRequest
success:nil
failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) {
XCTAssertNotNil(error);
XCTAssertTrue([error.domain isEqualToString:NSURLErrorDomain]);
XCTAssertTrue(error.code == NSURLErrorBadURL);
[expectation fulfill];
}];
[self waitForExpectationsWithCommonTimeout];
XCTAssertNil(downloadReceipt, @"downloadReceipt should be nil");
}
簡(jiǎn)單解釋一下上面代碼:
1.導(dǎo)入測(cè)試文件
2.聲明屬性
3.setUp方法設(shè)置屬性,初始化
4.tearDown中銷毀屬性
5.前兩個(gè)方法testThatImageDownloaderSingletonCanBeInitialized 和testThatImageDownloaderCanBeInitializedAndDeinitializedWithActiveDownloads :通過(guò)XCTAssertNotNil和 XCTAssertNil 判斷不為nil 和 為nil. 簡(jiǎn)單使用
6.下載方法測(cè)試:testThatImageDownloaderReturnsNilWithInvalidURL
首先是創(chuàng)建NSMutableURLRequest 和 AFImageDownloadReceipt 對(duì)象來(lái)下載圖片
然后在[self.downloader downloadImageForURLRequest...]方法中block回調(diào)進(jìn)行判斷:XCTAssertNotNil和XCTAssertTrue 等等.
關(guān)鍵是下面這行代碼:
[expectation fulfill];
fulfill:異步請(qǐng)求結(jié)束后需要調(diào)用expectation 的 fulfill方法, 通知測(cè)試異步請(qǐng)求已結(jié)束. 然后執(zhí)行下面等待超時(shí)的方法:
[self waitForExpectationsWithCommonTimeout];
點(diǎn)擊該方法,發(fā)現(xiàn)跳轉(zhuǎn)到AFTestCase文件中,最后發(fā)現(xiàn)執(zhí)行了下面代碼:
[self waitForExpectationsWithTimeout:self.networkTimeout handler:handler];
顯然該方法是指多少秒后超時(shí),因?yàn)檎?qǐng)求是需要時(shí)間的,設(shè)置Timeout就很有必要了.
所以對(duì)于異步執(zhí)行測(cè)試一般以下步驟(OC):
- (void)testExample {
//1: 創(chuàng)建XCTestExpectation對(duì)象
XCTestExpectation* expect = [self expectationWithDescription:@"請(qǐng)求超時(shí)timeout!"];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(5); //2: 假設(shè)請(qǐng)求需要耗時(shí)5秒
NSError *error = [[NSError alloc]init];//3: 假設(shè)回調(diào)返回一個(gè)error
XCTAssertNotNil(error); //4: 對(duì)結(jié)果進(jìn)行判斷
XCTAssertTrue([error.domain isEqualToString:NSURLErrorDomain]);
dispatch_async(dispatch_get_main_queue(), ^{
//主線程操作....
});
[expect fulfill];//5: 異步結(jié)束調(diào)用fulfill,告知請(qǐng)求結(jié)束
});
[self waitForExpectationsWithTimeout:15 handler:^(NSError *error) {
//6: 如果15秒內(nèi)沒(méi)有收到fulfill方法通知調(diào)用次方法
//超時(shí)后執(zhí)行一些操作:
}];
//7: 對(duì)象被回收
XCTAssertNil(expect, @"expect should be nil");
}
異步請(qǐng)求單元測(cè)試Swift代碼:
func testAsyncURLConnection(){
let URL = NSURL(string: "http://www.baidu.com")!
let expect = expectation(description: "GET \(URL)")
let session = URLSession.shared
let task = session.dataTask(with: URL as URL, completionHandler: {(data, response, error) in
XCTAssertNotNil(data, "返回?cái)?shù)據(jù)不應(yīng)該為空")
XCTAssertNil(error, "error應(yīng)該為nil")
expect.fulfill() //請(qǐng)求結(jié)束通知測(cè)試
if response != nil {
let httpResponse: HTTPURLResponse = response as! HTTPURLResponse
XCTAssertEqual(httpResponse.statusCode, 200, "請(qǐng)求失敗!")
DispatchQueue.main.async {
//主線程中干事情
}
} else {
XCTFail("請(qǐng)求失敗!")
}
})
task.resume()
//請(qǐng)求超時(shí)
waitForExpectations(timeout: (task.originalRequest?.timeoutInterval)!, handler: {error in
task.cancel()
})
}
三 : 單元測(cè)試之Mock使用
使用前需要參考Mock 介紹及下載
Mock是什么?
使用場(chǎng)景:
比如上面(1)異步加載測(cè)試:沒(méi)有網(wǎng)絡(luò)或者不佳時(shí),自行創(chuàng)建數(shù)據(jù). (2)復(fù)雜數(shù)據(jù)庫(kù)查詢:數(shù)據(jù)庫(kù)在內(nèi)網(wǎng)或者暫時(shí)無(wú)法查詢時(shí),自行創(chuàng)建數(shù)據(jù).(3)多重網(wǎng)絡(luò)交互:避免復(fù)雜交互,需要簡(jiǎn)化測(cè)試流程,等等才能得到返回?cái)?shù)據(jù)時(shí).
或者說(shuō)是,在測(cè)試過(guò)程中了罪,對(duì)于一些不容易構(gòu)造或不容易獲取的對(duì)象,此時(shí)你可以創(chuàng)建一個(gè)虛擬的對(duì)象(mock object)來(lái)完成測(cè)試, Mock卻很方便,它直接返回你需要的數(shù)據(jù),不用初始化對(duì)象,避免復(fù)雜的數(shù)據(jù)獲取過(guò)程:
如下網(wǎng)站給出的示例代碼片段:
- (void)testDisplaysTweetsRetrievedFromConnection
{
Controller *controller = [[[Controller alloc] init] autorelease];
//聲明id類型對(duì)象(不需要TwitterConnection類直接初始化對(duì)象)
id mockConnection = OCMClassMock([TwitterConnection class]);
controller.connection = mockConnection;
Tweet *testTweet = /* create a tweet somehow */;
NSArray *tweetArray = [NSArray arrayWithObject:testTweet];
//模擬返回?cái)?shù)據(jù)
OCMStub([mockConnection fetchTweets]).andReturn(tweetArray);
[controller updateTweetView];
}
比如創(chuàng)建tableview測(cè)試:
id mockTableView = [OCMockObject mockForClass:[UITableView class]];
UITableViewCell *cell = [[UITableViewCell alloc] init];
[[[mockTableView expect] andReturn:cell] dequeueReusableCellWithIdentifier:@"MockTableViewCell" forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
總而言之,Mock可以方便的創(chuàng)建你想要的object對(duì)象,并調(diào)用其公共方法.詳細(xì)Mock語(yǔ)法和使用這里不做介紹
四 : 性能耗時(shí)測(cè)試
當(dāng)項(xiàng)目創(chuàng)建完測(cè)試文件時(shí),OC就會(huì)自動(dòng)創(chuàng)建下面方法:
- (void)testPerformanceExample {
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}
這個(gè)方法意思是將耗時(shí)操作丟到measureBlock里就行了:
[self measureBlock:^{
// Put the code you want to measure the time of here.
NSMutableDictionary *dic = @{}.mutableCopy;
for (NSInteger i = 0; i < 10000; i++) {
NSString *obj = [NSString stringWithFormat:@"%ld",(long)i];
[dic setObject:obj forKey:obj];;
}
}];
測(cè)試后打印日志,其中有平均average: 0.011,所有耗時(shí)values: [0.012836, 0.015668, 0.012153, 0.010468, 0.011057, 0.009932, 0.010598, 0.010772, 0.010296, 0.010185],等等:
Test Case '-[ARKit_OCTests testPerformanceExample]' started.
/Users/niexiaobo/Desktop/demo/ARKit-OC/ARKit-OCTests/ARKit_OCTests.m:58: Test Case '-[ARKit_OCTests testPerformanceExample]'
measured [Time, seconds] average: 0.011, relative standard deviation: 14.609%,
values: [0.012836, 0.015668, 0.012153, 0.010468, 0.011057, 0.009932, 0.010598, 0.010772, 0.010296, 0.010185], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
Test Case '-[ARKit_OCTests testPerformanceExample]' passed (0.419 seconds).
傳統(tǒng)耗時(shí)測(cè)試:
NSTimeInterval start = CACurrentMediaTime();
NSMutableDictionary *dic = @{}.mutableCopy;
for (NSInteger i = 0; i < 10000; i++) {
NSString *obj = [NSString stringWithFormat:@"%ld",(long)i];
[dic setObject:obj forKey:obj];;
}
NSLog(@"%lf",CACurrentMediaTime() - start);
五 : 單例測(cè)試
定義單例,在公共頭文件導(dǎo)入宏定義:
#define singleH(name) +(instancetype)share##name;
#if __has_feature(objc_arc)
#define singleM(name) static id _instance;\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
\
+(instancetype)share##name\
{\
return [[self alloc]init];\
}\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}
#else
#define singleM static id _instance;\
+(instancetype)allocWithZone:(struct _NSZone *)zone\
{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
\
+(instancetype)shareTools\
{\
return [[self alloc]init];\
}\
-(id)copyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
-(id)mutableCopyWithZone:(NSZone *)zone\
{\
return _instance;\
}\
-(oneway void)release\
{\
}\
\
-(instancetype)retain\
{\
return _instance;\
}\
\
-(NSUInteger)retainCount\
{\
return MAXFLOAT;\
}
#endif
既然單例的目的是不管怎么初始化創(chuàng)建對(duì)象永遠(yuǎn)都是返回唯一且相同的那個(gè),那么測(cè)試也一樣,測(cè)試不同,重復(fù)的方法應(yīng)該返回同一對(duì)象,并且可用:
- (void)testFilesManagerSingle
{
NSMutableArray *managerArray = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FilesManager *tempManager = [[FilesManager alloc] init];
[managerArray addObject:tempManager];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FilesManager *tempManager = [[FilesManager alloc] init];
[managerArray addObject:tempManager];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FilesManager *tempManager = [FilesManager shareManager];
[managerArray addObject:tempManager];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FilesManager *tempManager = [FilesManager shareManager];
[managerArray addObject:tempManager];
});
FilesManager *managerObj = [FilesManager shareManager];
[managerArray enumerateObjectsUsingBlock:^(FilesManager *obj, NSUInteger idx, BOOL * _Nonnull stop) {
XCTAssertEqual(managerObj, obj, @"FilesManager is not single");
}];
}
然后測(cè)試FilesManager的open和close等方法是否正常.等等
六 : 編寫測(cè)試用例該注意要點(diǎn)
(1) 要注意創(chuàng)建完成一個(gè)測(cè)試文件自動(dòng)創(chuàng)建的幾個(gè)方法:
- (void)setUp {
[super setUp];
}
- (void)tearDown {
}
使用:
我們?cè)诜椒╯etup()中聲明并創(chuàng)建一個(gè)Test對(duì)象
然后在方法tearDown()中釋放它. (有點(diǎn)像init 和 dealloc )
(2) 異步和性能測(cè)試往往比較耗時(shí),所以要注意和邏輯測(cè)試等分開測(cè)試
(3) 測(cè)試框架有好幾個(gè),對(duì)于中小型項(xiàng)目個(gè)人覺(jué)得考慮兼容性直接使用XCTest
(4) 公用方法等盡量抽離或者寫一個(gè)宏,比如本節(jié)中單例,或者[self waitForExpectationsWithCommonTimeout]; 方法寫一個(gè)TimeoutTest宏等等.
七 : 封裝測(cè)試庫(kù)
當(dāng)你的測(cè)試內(nèi)容越來(lái)越多時(shí),測(cè)試代碼就像工程一樣,甚至更復(fù)雜, 同樣單元測(cè)試也需要封裝,繼承,設(shè)計(jì)等等.
比如上面第二節(jié)里異步測(cè)試,AFImageDownloaderTests測(cè)試文件繼承自AFTestCase:
八 : 自動(dòng)化測(cè)試,Jenkins的安裝和使用
[編輯中]