iOS單元測(cè)試及其應(yīng)用

0.緒論

基礎(chǔ)概念:BDD(Behavior Driven Development-行為驅(qū)動(dòng)開(kāi)發(fā))與TDD(Test Driven Development-測(cè)試驅(qū)動(dòng)開(kāi)發(fā))

TDD-測(cè)試驅(qū)動(dòng)開(kāi)發(fā)

Test-driven development 測(cè)試驅(qū)動(dòng)開(kāi)發(fā)盾致,它是一種用一種測(cè)試先于編寫(xiě)代碼的思想來(lái)指導(dǎo)軟件開(kāi)發(fā)班缰。測(cè)試驅(qū)動(dòng)開(kāi)發(fā)是敏捷開(kāi)發(fā)中的一項(xiàng)核心實(shí)踐和技術(shù),其原理是在開(kāi)發(fā)功能代碼之前,先編寫(xiě)單元測(cè)試用例代碼俩垃,從而通過(guò)測(cè)試代碼確定需要編寫(xiě)什么具體的開(kāi)發(fā)代碼。使用這種做法的結(jié)果是一套全面的單元測(cè)試,可以隨時(shí)運(yùn)行以提供軟件可以正常工作的反饋。

BDD-行為驅(qū)動(dòng)開(kāi)發(fā)

Behavior-Driven Development透绩,行為驅(qū)動(dòng)開(kāi)發(fā)是指以軟件實(shí)際要達(dá)到的目標(biāo)也就是需求設(shè)計(jì)來(lái)驅(qū)動(dòng)整個(gè)開(kāi)發(fā)過(guò)程,更關(guān)注于用戶故事壁熄,即產(chǎn)品帚豪、開(kāi)發(fā)者、測(cè)試人員一起頭腦風(fēng)暴草丧,分析實(shí)際對(duì)應(yīng)用戶對(duì)于該軟件的需求狸臣,然后將這些需求寫(xiě)成一個(gè)個(gè)的故事。BDD重點(diǎn)是軟件開(kāi)發(fā)過(guò)程中使用的語(yǔ)言和交互昌执。開(kāi)發(fā)者負(fù)責(zé)填充故事的內(nèi)容滿足用戶需求烛亦,測(cè)試者負(fù)責(zé)檢驗(yàn)這些故事的結(jié)果是否是用戶所希望的成果诈泼。相比較TDD來(lái)說(shuō),這種開(kāi)發(fā)形式可以盡可能的避免用戶和開(kāi)發(fā)者在溝通上的障礙煤禽,實(shí)現(xiàn)客戶和開(kāi)發(fā)者同時(shí)定義系統(tǒng)的需求铐达,避免因?yàn)槔斫庑枨蟛怀浞侄鴰?lái)的不必要的工作量。

1.什么是單元測(cè)試

單元測(cè)試: Unit Testing檬果,顧名思義就是對(duì)最小的一個(gè)單元代碼或不受其他代碼邏輯影響的函數(shù)等進(jìn)行測(cè)試娶桦,對(duì)輸入的參數(shù)和輸出都要求比較明確。單元測(cè)試的目的是把程序里每個(gè)獨(dú)立的最小單元隔離開(kāi)來(lái)汁汗,保證其正確性,使得整個(gè)程序相對(duì)也更正確栗涂。(單元測(cè)試與接口測(cè)試的區(qū)別

2.為什么要做單元測(cè)試

  • 單元測(cè)試作為敏捷開(kāi)發(fā)實(shí)踐的組成之?知牌,?的是提?軟件開(kāi)發(fā)的效率,維持代碼的健壯性斤程,對(duì)我們的產(chǎn)品質(zhì)量是非常重要的
  • 單元測(cè)試是所有測(cè)試中最底層的一類測(cè)試角寸,是第一個(gè)環(huán)節(jié),也是最重要的一個(gè)環(huán)節(jié)忿墅,是唯一一次有保證能夠代碼覆蓋率達(dá)到100%的測(cè)試扁藕,是整個(gè)軟件測(cè)試過(guò)程的基礎(chǔ)和前提,單元測(cè)試防止了開(kāi)發(fā)的后期因bug過(guò)多而失控疚脐,單元測(cè)試的性價(jià)比是最好的
  • 據(jù)統(tǒng)計(jì)亿柑,大約有80%的錯(cuò)誤是在軟件設(shè)計(jì)階段引入的,并且修正一個(gè)軟件錯(cuò)誤所需的費(fèi)用將隨著軟件生命期的進(jìn)展而上升棍弄。錯(cuò)誤發(fā)現(xiàn)的越晚望薄,修復(fù)它的費(fèi)用就越高,而且呈指數(shù)增長(zhǎng)的趨勢(shì)
  • 代碼規(guī)范呼畸、優(yōu)化痕支,可測(cè)試性的代碼
  • 放心重構(gòu)
  • 自動(dòng)化執(zhí)行three-thousand times
  • 單元測(cè)試也有?些?級(jí)的作?,?如支持web?動(dòng)化測(cè)試蛮原、APP自動(dòng)化測(cè)試卧须、API接口自動(dòng)化測(cè)試等各種自動(dòng)化測(cè)試

3.iOS單元測(cè)試?案

測(cè)試框架:

Github上的一些知名的開(kāi)源庫(kù)使用的測(cè)試框架:

常用的測(cè)試框架:

  • XCTest:Apple自帶,與Xcode深度集成且享受Apple后續(xù)對(duì)XCTest升級(jí)維護(hù)儒陨。
  • KiWi:第三方插件花嘶,面向BDD思想

AFNetworking是一個(gè)輕量級(jí)的iOS網(wǎng)絡(luò)通信類庫(kù)。它建立在NSURLConnection和NSOperation等類庫(kù)的基礎(chǔ)上框全,讓很多網(wǎng)絡(luò)通信功能的實(shí)現(xiàn)變得十分簡(jiǎn)單察绷,同時(shí)它支持HTTP請(qǐng)求和基于REST的網(wǎng)絡(luò)服務(wù)(包括GET、POST津辩、 PUT拆撼、DELETE等)容劳,且支持ARC。

采用?案:XCTest + OCMock (mock對(duì)象闸度、樁程序)

測(cè)試對(duì)象:

?絡(luò)請(qǐng)求接?
?具類?法
部分可測(cè)試視圖邏輯
私有?法

測(cè)試工具:

1.XCTest

XCTest是Xcode集成的?套單元測(cè)試框架竭贩,以下是?些基本使? :

基本?法:

- (void)setUp {
[super setUp];
// 每個(gè)test?法執(zhí)?前調(diào)?,在這個(gè)測(cè)試?例?進(jìn)??些通?的初始化?作
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tearDown {
[super tearDown];
// 每個(gè)test?法執(zhí)?后調(diào)?
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
- (void)testExample {
// 測(cè)試?法樣例 // This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
- (void)testPerformanceExample {
//這個(gè)?法主要是做性能測(cè)試的莺禁,所謂性能測(cè)試留量,主要就是評(píng)估?段代碼的運(yùn)?時(shí)間。該?法就是性能測(cè)試?法的樣例哟冬。
// This is an example of a performance test case.
[self measureBlock:^{
// Put the code you want to measure the time of here.
}];
}

UT三步曲:

  1. 準(zhǔn)備數(shù)據(jù)
  2. 調(diào)用方法
  3. 斷言結(jié)果, 判斷查看結(jié)果

示例代碼:

//書(shū)寫(xiě)規(guī)范:方法名必須 以 “test” 開(kāi)頭
- (void)testStatus_fLevel {
    //1.數(shù)據(jù)準(zhǔn)備
    BMKMapStatus *status = [[BMKMapStatus alloc] init];
    status.fLevel = 12.0;
 
 
    //2.調(diào)用方法
    [_mapView setMapStatus:status];
    BMKMapStatus *rstatus = [_mapView getMapStatus];
 
 
    //3.斷言結(jié)果, 判斷查看結(jié)果
    XCTAssertEqualWithAccuracy(rstatus.fLevel, 12.0, 0.1, "flevel wrong");
}
 
 
注:對(duì)于該用例楼熄,代碼執(zhí)行順序:setUp--->testStatus_fLevel--->tearDown

注意事項(xiàng):

  • 1.整個(gè)工程中應(yīng)該有多個(gè)XCTest文件,每一個(gè)XCTests都是只有.m文件的浩峡,繼承自XCTestCase(當(dāng)然如果你有公共配置可岂,其實(shí)可以先新建繼承自XCTestCase類的一個(gè)Cocoa Touch Class類(如UserDefinedBaseTestCase)當(dāng)作自己的XCTest所有類的父類,然后對(duì)于測(cè)試的基本配置/公用方法都可以在這個(gè)父類中實(shí)現(xiàn)翰灾;其他子類文件可以直接繼承自UserDefinedBaseTestCase類就可以直接調(diào)用公有方法了)
  • 2.每個(gè)XCTests.m文件中缕粹,必然包含一個(gè)setUp方法和一個(gè)tearDown方法
  • 3.每個(gè)單元測(cè)試方法執(zhí)行之前,XCTest會(huì)先執(zhí)行setUp方法纸淮,所以我們可以在這個(gè)方法中進(jìn)行初始化參數(shù)或測(cè)試環(huán)境的配置
  • 4.每個(gè)單元測(cè)試方法執(zhí)行結(jié)束后平斩,XCTest會(huì)執(zhí)行tearDown方法,所以可以把需要測(cè)試完成后銷毀的內(nèi)容邏輯放在這里咽块,以便保證下面的測(cè)試不受本次測(cè)試影響
  • 5.每個(gè)單元測(cè)試方法都應(yīng)該以test開(kāi)頭

斷?:

XCTAssert(expression, ...) XCTAssert(條件, 不滿?條件的描述)

eg.
XCTAssert(a == 0, @“a不能等于0”)

真假斷言:Test a condition that generates a true or false result

XCTAssertTrue(expression, ...) 當(dāng)expression == false時(shí)報(bào)錯(cuò)

XCTAssertFalse(expression, ...) 當(dāng)expression != false時(shí)報(bào)錯(cuò)

等價(jià)斷言:Check whether two values are equal or not equal.

XCTAssertEqualObjects(expression1, expression2, format...) 當(dāng)expression1不等于expression2時(shí)報(bào)錯(cuò)
XCTAssertNotEqualObjects(expression1, expression2, format...) 當(dāng)expression1 等于expression2時(shí)報(bào)錯(cuò)

XCTAssertEqual(expression1, expression2, format...) 當(dāng)expression1不等于 expression2時(shí)報(bào)錯(cuò)绘面,這個(gè)斷言?于C語(yǔ)?的標(biāo)量
XCTAssertNotEqual(expression1, expression2, format...) 當(dāng)expression1等于 expression2時(shí)報(bào)錯(cuò),這個(gè)斷言?于C語(yǔ)?的標(biāo)量

XCTAssertEqualWithAccuracy(expression1, expression2, accuracy, format...) 當(dāng) expression1和expression2之間的差別?于accuracy將報(bào)錯(cuò)糜芳。這種斷言適?于 floats和doubles這些標(biāo)量(C scalar type)飒货,兩者之間的細(xì)微差異導(dǎo)致它們不完全相等,但是對(duì)所有的標(biāo)量都有效
XCTAssertNotEqualWithAccuracy(expression1, expression2, accuracy, format...) 當(dāng)expression1和expression2之間的差別低于accuracy將報(bào)錯(cuò)峭竣。這種斷言適?于floats和doubles這些標(biāo)量塘辅,兩者之間的細(xì)微差異導(dǎo)致它們不完全相等,但是對(duì)所有的標(biāo)量都有效

空/非空斷言:Check whether a test condition is nil or non-nil.

XCTAssertNil(expression, format...) 當(dāng)expression參數(shù)?nil時(shí)報(bào)錯(cuò)
XCTAssertNotNil(expression, format...) 當(dāng)expression參數(shù)為nil時(shí)報(bào)錯(cuò)

錯(cuò)誤斷言:Check whether a function call throws (or doesn’t throw) an error.

XCTAssertThrows(expression, format...) 當(dāng)expression不拋出異常時(shí)報(bào)錯(cuò)
XCTAssertThrowsSpecific(expression, exception_class, format...) 當(dāng)expression 針對(duì)指定類不拋出異常時(shí)報(bào)錯(cuò)
XCTAssertThrowsSpecificNamed(expression, exception_class, exception_name, format...) 當(dāng)expression針對(duì)特定類和特定名字不拋出異常時(shí)報(bào)錯(cuò)皆撩。對(duì)于AppKit框 架或Foundation框架?常有?扣墩,拋出帶有特定名字的NSException(NSInvalidArgumentException等等)
XCTAssertNoThrow(expression, format...) 當(dāng)expression拋出異常時(shí)報(bào)錯(cuò)
XCTAssertNoThrowSpecific(expression, exception_class, format...) 當(dāng) expression針對(duì)指定類拋出異常時(shí)報(bào)錯(cuò)。任意其他異常都可以扛吞;也就是說(shuō)它不會(huì)報(bào) 錯(cuò)
XCTAssertNoThrowSpecificNamed(expression, exception_class, exception_name, format...) 當(dāng)expression針對(duì)特定類和特定名字拋出異常時(shí)報(bào)錯(cuò)呻惕。
對(duì)于AppKit框架或Foundation框架?常有?,拋出帶有特定名字的 NSException(NSInvalidArgumentException等

無(wú)條件失敗斷言:Generate a failure immediately and unconditionally.

XCTFail(...) 無(wú)條件返回失敗

異步操作測(cè)試:

有些需要測(cè)試的方法可能會(huì)執(zhí)行異步網(wǎng)絡(luò)操作滥比,也許并不能立即返回服務(wù)器響應(yīng)的結(jié)果亚脆,這就需要我們考慮實(shí)現(xiàn)異步操作的單元測(cè)試,而XCTestCase內(nèi)置支持測(cè)試異步方法

  • (XCTestExpectation *)expectationWithDescription:(NSString *)description:方法獲取XCTestExpectation的實(shí)例盲泛,在這一模式下濒持,測(cè)試完成的方法并不會(huì)被標(biāo)記為測(cè)試用例通過(guò)键耕;
  • (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(nullable XCWaitCompletionHandler)handler:方法用來(lái)等待操作完成,如果測(cè)試用例沒(méi)有完成柑营,那么將會(huì)調(diào)用回調(diào)處理的塊
  • (void)fulfill:XCTestExpectation為fulfilled狀態(tài)的話代表異步操作已經(jīng)完成屈雄,可以檢測(cè)是否符合預(yù)期

代碼覆蓋率

在自動(dòng)化單元測(cè)試或功能測(cè)試中,經(jīng)常使用代碼覆蓋率用來(lái)表示經(jīng)過(guò)測(cè)試的代碼的百分比官套,以保證我們的代碼能夠經(jīng)過(guò)足夠的測(cè)試酒奶。
我們可以開(kāi)通xcode的代碼覆蓋收集,開(kāi)通后就可以在導(dǎo)航欄的報(bào)告導(dǎo)航欄中看到測(cè)試記錄及結(jié)果奶赔,還可以選擇點(diǎn)擊??跳轉(zhuǎn)到測(cè)試代碼處惋嚎,還可以點(diǎn)擊Coverage或者Logs標(biāo)簽查看覆蓋代碼和測(cè)試日志

image.png

2.OCMock

我們要測(cè)試的?法會(huì)引?很多外部依賴的對(duì)象,?我們沒(méi)法控制這些外部依賴的對(duì)象站刑。為了解決這個(gè)問(wèn)題瘸彤,我們需要?到Stub和Mock來(lái)模擬這些外部依賴的對(duì)象,從?控制它們。單獨(dú)依靠XCTest難以完成Mock或者Stub但是結(jié)合OCMock可以在測(cè)試代碼中實(shí)現(xiàn)這以下功能:
● Stub --- 樁程序, ?為地讓?個(gè)對(duì)象對(duì)某個(gè)?法返回我們事先規(guī)定好的值
eg.
OCMStub([mockCar getCarBrand:[OCMArg any]]).andReturn(@"XXX");
[OCMArg any]表示可以為任意參數(shù)笛钝,若改為具體參數(shù)(如:張三),表示只有參數(shù) 為張三時(shí)才會(huì)觸發(fā)stup愕宋,否則不會(huì)觸發(fā)

● Mock --- 模擬對(duì)象, ?個(gè)對(duì)象, 它是對(duì)現(xiàn)有類的?為?種模擬(或是對(duì)現(xiàn)有接?實(shí)現(xiàn)的模擬), 它只能響應(yīng)那些你添加了期望或者 stub 的?法
eg.
// 1.nice mock - 不會(huì)在?個(gè)沒(méi)有被stub的?法被調(diào)?時(shí)拋出異常
Car *mockCar = OCMClassMock([Car class]);
// 2.vanilla mock - 在mock的?命周期中每?個(gè)?法調(diào)?都必須是stub過(guò)的?法玻靡。當(dāng)調(diào)??個(gè)沒(méi)有stub的?法的時(shí)候會(huì)拋出?個(gè)異常
Car *mockCar = OCMStrictClassMock([Car class]);
// 3.partial mock - 當(dāng)?個(gè)沒(méi)有stub過(guò)的?法被調(diào)?了,這個(gè)?法會(huì)被轉(zhuǎn)發(fā)到真 實(shí)的對(duì)象上中贝。
Car *mockCar = OCMPartialMock([Car class]);

4.示例Demo

4.1 測(cè)試步驟

  1. 給定待測(cè)的接口囤捻、代碼模塊以及相應(yīng)API文檔
  2. 熟悉接口、理清內(nèi)部邏輯邻寿、分支結(jié)構(gòu)等細(xì)節(jié)
  3. 設(shè)計(jì)用例蝎土、編寫(xiě)單元測(cè)試
  4. 執(zhí)行,斷言绣否,觀察結(jié)果和預(yù)期

4.2 案例實(shí)現(xiàn)

  1. 對(duì)于給定接口(網(wǎng)絡(luò)請(qǐng)求類接口)誊涯,通過(guò)接口注釋文檔明確其接口功能,輸入?yún)?shù)蒜撮、輸出結(jié)果暴构、請(qǐng)求方式等信息:
    -接口功能:公交路線檢索(僅支持市內(nèi))
    -輸入?yún)?shù):BMKTransitRoutePlanOption對(duì)象
    -返回結(jié)果:BOOL 值,YES/NO
    -請(qǐng)求類型:異步請(qǐng)求(即請(qǐng)求不在主線程操作)
/**
 *公交路線檢索(僅支持市內(nèi))
 *異步函數(shù)段磨,返回結(jié)果在BMKRouteSearchDelegate的onGetTransitRouteResult通知
 *@param transitRoutePlanOption 公交換乘信息類
 *@return 成功返回YES取逾,否則返回NO
 */
- (BOOL)transitSearch:(BMKTransitRoutePlanOption *)transitRoutePlanOption;
  1. 通過(guò)代碼走讀(查看接口具體實(shí)現(xiàn)),熟悉接口苹支、理清內(nèi)部邏輯砾隅、分支結(jié)構(gòu)等細(xì)節(jié):
    -入?yún)⒌暮戏ㄐ孕r?yàn)
    -代碼分支結(jié)構(gòu)是否合理
    -代碼魯棒性(健壯性、可靠性)
此處省略一萬(wàn)字
  1. 設(shè)計(jì)用例债蜜、編寫(xiě)單元測(cè)試
    -保證每個(gè)單元測(cè)試用例覆蓋最小粒度的代碼邏輯
    -全部單元測(cè)試用例疊加起來(lái)盡最大可能覆蓋待測(cè)接口(模塊)全部代碼行晴埂、條件分支
#import <XCTest/XCTest.h>
#import <xxx_Search/BMKSearchComponent.h>
@interface RoutePlanTransitSearchTest : XCTestCase<BMKRouteSearchDelegate>
{
    BMKRouteSearch *routeSearch;
    XCTestExpectation *expectation;
    int tag;
}
@end
 
@implementation RoutePlanTransitSearchTest
 
- (void)setUp {
    [super setUp];
    routeSearch = [[BMKRouteSearch alloc]init];
    routeSearch.delegate = self;
     
}
 
- (void)tearDown {
    routeSearch.delegate = nil;
    routeSearch = nil;
    expectation = nil;
    [super tearDown];
}
 
/**
 * "定義一個(gè)TransitRoutePlanOption對(duì)象
 * 1究反、option為null
 * 驗(yàn)證:返回NO
 */
- (void)testTransitRoutePlanSearch_Option_Null{
    Boolean flag = [routeSearch transitSearch:nil];
    XCTAssertFalse(flag, "option null return NO");
}
 
/**
 * "定義一個(gè)TransitRoutePlanOption對(duì)象
 * 1、from節(jié)點(diǎn)為null
 * 2邑时、to節(jié)點(diǎn)正常"
 * 3奴紧、city正常
 * 驗(yàn)證:返回NO
 */
- (void)testTransitRoutePlanSearch_From_Null{
    
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    end.cityName = @"北京";
    end.name = @"百度大廈";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = nil;
    option.to = end;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertFalse(flag, "from null return NO");
}
 
/**
 * "定義一個(gè)TransitRoutePlanOption對(duì)象
 * 1、from節(jié)點(diǎn)正常
 * 2晶丘、to節(jié)點(diǎn)null
 * 3黍氮、city正常
 * 驗(yàn)證:返回NO
 */
- (void)testTransitRoutePlanSearch_To_Null{
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"奎科科技大廈";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = nil;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertFalse(flag, "to null return NO");
}
 
/**
 * "定義一個(gè)BMKTransitRoutePlanOption對(duì)象
 * 1、from節(jié)點(diǎn)正常
 * 2浅浮、to節(jié)點(diǎn)正常
 * 3沫浆、city為null,即未指定在哪個(gè)城市檢索
 * 驗(yàn)證:端上攔截滚秩,請(qǐng)求失敗专执,返回NO
 */
- (void)testTransitRoutePlanSearch_City_Null{
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"奎科科技大廈";
    end.cityName = @"北京";
    end.name = @"百度大廈";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = nil;
    option.from = start;
    option.to = end;
    Boolean flag = [routeSearch transitSearch:option];//預(yù)期:端上攔截,請(qǐng)求失敗郁油,返回NO
    XCTAssertFalse(flag, @"起終點(diǎn)使用關(guān)鍵字時(shí)本股,必須設(shè)定城市,不能為空或者空字符串");
    NSLog(@"testTransitRoutePlanSearch_City_Null flag %d", flag);
}
 
//city.length > MAX_CITY_LENGTH, 返回NO
- (void)testTransitRoutePlanSearch_Max_City{
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"奎科科技大廈";
    end.cityName = @"北京";
    end.name = @"天通苑北一區(qū)";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京北京";
    option.from = start;
    option.to = end;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertFalse(flag, @"city lenght > max return false");
}
 
/**
 * "定義一個(gè)TransitRoutePlanOption對(duì)象
 * 1桐腌、from的PlanNode節(jié)點(diǎn)拄显,如北京,奎科科技大廈
 * 2案站、to的PlanNode節(jié)點(diǎn)躬审,如北京,天通苑北一區(qū)"
 * 驗(yàn)證:
 * "返回的TransitRouteResult不為空蟆盐,errorcode為no_error
 * 1承边、getRouteLines()不為空
 * 2、每條TransitRouteLine中g(shù)etWayPoints()不為空,getAllStep()不為空
 * 3石挂、每個(gè)路段節(jié)點(diǎn)博助,getEntrance()、getStepType()痹愚、getExit()翔始、getInstructions()、getVehicleInfo()里伯、不為空
 * 4城瞎、當(dāng)路段公交或地鐵,VehicleInfo中g(shù)etPassStationNum()疾瓮、getTitle()脖镀、getTotalPrice()、getUid()、getZonePrice()不為空"
 */
- (void)testTransitRoutePlanSearch_Normal{
    tag = 1;
    expectation = [self expectationWithDescription:@"search result"];
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"奎科科技大廈";
    end.cityName = @"北京";
    end.name = @"天通苑北一區(qū)";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = end;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertTrue(flag, @"normal search return ture");
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
}
 
- (void)testTransitRoutePlanSearch_Normal_Pt{
    tag = 1;
    expectation = [self expectationWithDescription:@"search result"];
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.pt = CLLocationCoordinate2DMake(40.0477880000,116.3132600000);
    end.pt = CLLocationCoordinate2DMake(40.0830240000,116.4239230000);
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = end;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertTrue(flag, @"normal search return ture");
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
}
/**
 * "定義一個(gè)TransitRoutePlanOption對(duì)象
 * 1蜒灰、from的PlanNode節(jié)點(diǎn)弦蹂,通過(guò)withCityCodeAndPlaceName設(shè)置,如131强窖,奎科科技大廈
 * 2凸椿、to的PlanNode節(jié)點(diǎn),通過(guò)withCityNameAndPlaceName設(shè)置翅溺,如北京脑漫,天通苑北一區(qū)
 * 3、policy為EBUS_NO_SUBWAY"
 *
 * "返回的TransitRouteResult不為空咙崎,errorcode為no_error
 * 1优幸、getRouteLines()不為空
 * 2、每條TransitRouteLine中g(shù)etWayPoints()不為空褪猛,getAllStep()不為空
 * 3网杆、每個(gè)路段節(jié)點(diǎn),getEntrance()伊滋、getStepType()不為SUBWAY,getExit()碳却、getInstructions()、getVehicleInfo()笑旺、不為空
 * 4追城、當(dāng)路段公交或地鐵,VehicleInfo中g(shù)etPassStationNum()燥撞、getTitle()、getTotalPrice()迷帜、getUid()物舒、getZonePrice()不為空"
 */
- (void)testTransitRoutePlanSearch_Normal_With_Policy{
    tag = 1;
    expectation = [self expectationWithDescription:@"search result"];
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"奎科科技大廈";
    end.cityName = @"北京";
    end.name = @"天通苑北一區(qū)";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = end;
    option.transitPolicy = BMK_TRANSIT_NO_SUBWAY;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertTrue(flag, @"normal search return ture");
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
}
 
/**
 * "定義一個(gè)TransitRoutePlanOption對(duì)象
 * 1、from的PlanNode節(jié)點(diǎn)戏锹,通過(guò)withCityCodeAndPlaceName設(shè)置冠胯,如131,”窩窩窩“
 * 2锦针、to的PlanNode節(jié)點(diǎn)荠察,通過(guò)withCityNameAndPlaceName設(shè)置,如北京奈搜,”哈哈哈哈“
 * 返回的TransitRouteResult不為空悉盆,errorcode為result_not_found
 */
- (void)testTransitRoutePlanSearch_Result_Not_Found{
    tag = 2;
    expectation = [self expectationWithDescription:@"search result"];
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"gute street";
    end.cityName = @"北京";
    end.name = @"gute platz";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = end;
    option.transitPolicy = BMK_TRANSIT_NO_SUBWAY;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertTrue(flag, @"normal search return true");
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
    }
 
- (void)testTransitRoutePlanSearch_Result_Ambiguous{
    tag = 3;
    expectation = [self expectationWithDescription:@"search result"];
    BMKPlanNode* start = [[BMKPlanNode alloc]init];
    BMKPlanNode* end = [[BMKPlanNode alloc]init];
    start.cityName = @"北京";
    start.name = @"百度大廈";
    end.cityName = @"北京";
    end.name = @"小明";
    BMKTransitRoutePlanOption *option = [[BMKTransitRoutePlanOption alloc] init];
    option.city = @"北京市";
    option.from = start;
    option.to = end;
    option.transitPolicy = BMK_TRANSIT_NO_SUBWAY;
    Boolean flag = [routeSearch transitSearch:option];
    XCTAssertTrue(flag, @"normal search return true");
    [self waitForExpectationsWithTimeout:5.0 handler:nil];
     
}
 
- (void)onGetTransitRouteResult:(BMKRouteSearch*)searcher result:(BMKTransitRouteResult*)result errorCode:(BMKSearchErrorCode)error{
    switch (tag) {
        case 1:
            XCTAssertEqual(error, BMK_SEARCH_NO_ERROR, @"error code wrong");
            XCTAssertNotNil(result, @"result nil");
            XCTAssertNotNil(result.routes, @"routes nil");
       
            for (BMKTransitRouteLine* plan in result.routes) {
                XCTAssertTrue(plan.distance > 0, @"plan distance 0");
                XCTAssertNotNil(plan.duration, @"plan duration nil");
                XCTAssertNotNil(plan.starting, @"plan start nil");
                XCTAssertNotNil(plan.terminal, @"plan end nil");
                XCTAssertNotNil(plan.steps, @"plan steps nil");
            }
            break;
        case 2:
            XCTAssertEqual(error, BMK_SEARCH_RESULT_NOT_FOUND, @"error code wrong");
            XCTAssertNil(result, @"result nil");
            break;
        case 3:
            XCTAssertEqual(error, BMK_SEARCH_AMBIGUOUS_ROURE_ADDR, @"error code wrong");
            XCTAssertNotNil(result, @"result nil");
            XCTAssertNotNil(result.suggestAddrResult, @"suggestAddrResult nil");
            XCTAssertNil(result.routes, @"routes nil");
            break;
        default:
            break;
    }
    [expectation fulfill];
}
 
@end
  1. 執(zhí)行,斷言馋吗,觀察結(jié)果和預(yù)期


    image.png

5.思考

  1. 單元測(cè)試的覆蓋率越全越好焕盟,當(dāng)覆蓋率(分支覆蓋率、行覆蓋率)達(dá)到百分百時(shí)就能說(shuō)明待測(cè)接口或模塊沒(méi)有問(wèn)題了宏粤?
    是否考慮代碼分支邏輯的正確性

  2. 單元測(cè)試脚翘,除了關(guān)注待測(cè)對(duì)象的功能正確性之外灼卢,是否還需要關(guān)注其他方面?
    用戶體驗(yàn):錯(cuò)誤碼類型来农、日志提示的合理性|可讀性

  3. 對(duì)于網(wǎng)絡(luò)請(qǐng)求類接口鞋真,如何保證case運(yùn)行的穩(wěn)定性?
    考慮網(wǎng)絡(luò)延遲沃于,延遲操作涩咖,等待返回結(jié)果,具體方法:
    (1)在主線程中適當(dāng)sleep揽涮,等待返回結(jié)果
    (2)在單測(cè)內(nèi)部延遲判斷

_expectation = [self expectationWithDescription:@"search result"];
 
BMKReverseGeoCodeSearchOption *rgcOption = [[BMKReverseGeoCodeSearchOption alloc] init];
rgcOption.location = CLLocationCoordinate2DMake(40.049850, 116.279920);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    BOOL result = [_geoSearch reverseGeoCode:rgcOption];
    XCTAssertTrue(result, @"geoSearch should return true");
});
 
[self waitForExpectationsWithTimeout:5.0 handler:nil];

參考文獻(xiàn)

  1. https://developer.apple.com/documentation/xctest
  2. https://ocmock.org/
  3. https://github.com/kiwi-bdd/Kiwi
  4. https://www.zhihu.com/question/28729261

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抠藕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蒋困,更是在濱河造成了極大的恐慌盾似,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雪标,死亡現(xiàn)場(chǎng)離奇詭異零院,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)村刨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)告抄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人嵌牺,你說(shuō)我怎么就攤上這事打洼。” “怎么了逆粹?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵募疮,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我僻弹,道長(zhǎng)阿浓,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任蹋绽,我火速辦了婚禮芭毙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘卸耘。我一直安慰自己退敦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布蚣抗。 她就那樣靜靜地躺著苛聘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上设哗,一...
    開(kāi)封第一講書(shū)人閱讀 52,549評(píng)論 1 312
  • 那天唱捣,我揣著相機(jī)與錄音,去河邊找鬼网梢。 笑死震缭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的战虏。 我是一名探鬼主播拣宰,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼烦感!你這毒婦竟也來(lái)了巡社?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤手趣,失蹤者是張志新(化名)和其女友劉穎晌该,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體绿渣,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡朝群,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了中符。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姜胖。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖淀散,靈堂內(nèi)的尸體忽然破棺而出右莱,到底是詐尸還是另有隱情,我是刑警寧澤档插,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布慢蜓,位于F島的核電站,受9級(jí)特大地震影響阀捅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜针余,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一饲鄙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧圆雁,春花似錦忍级、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春朴肺,著一層夾襖步出監(jiān)牢的瞬間窖剑,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工戈稿, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留西土,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓鞍盗,卻偏偏與公主長(zhǎng)得像需了,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子般甲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 引言:因?yàn)橹肮ぷ髦薪?jīng)歷過(guò)幾次大的項(xiàng)目重構(gòu)和組件化肋乍,所以陸陸續(xù)續(xù)學(xué)習(xí)了一些iOS單元測(cè)試相關(guān)的一些知識(shí),以下內(nèi)容是...
    RyanYuan閱讀 11,528評(píng)論 4 68
  • 簡(jiǎn)單使用單元測(cè)試 對(duì)方法引用AFN框架的單元測(cè)試 不寫(xiě)單元測(cè)試的程序員是不合格的敷存,為了讓自己成為一名合格的程序員墓造,...
    sunmumu1222閱讀 280評(píng)論 0 0
  • 一、單元測(cè)試的定義 在計(jì)算機(jī)編程中历帚,單元測(cè)試(英語(yǔ):Unit Testing)又稱為模塊測(cè)試, 是針對(duì)程序模塊(軟...
    草原烈鷹閱讀 475評(píng)論 0 1
  • XCTestCase 進(jìn)入xxUITests.m文件滔岳,會(huì)看到繼承自XCTestCase:Xcode集成的一套單元測(cè)...
    我叫Vincent閱讀 1,741評(píng)論 0 2
  • Xcode測(cè)試 前言 總算在今天把單元測(cè)試的官方文檔翻譯寫(xiě)成了一片博客。首先感謝黨挽牢,感謝人民谱煤,感謝我的父母。也必須...
    許漠顏閱讀 3,364評(píng)論 10 34