測試目的:模擬多種可能性,減少錯誤,增強健壯性、提高穩(wěn)定性,減少代碼修改導致未知的問題等。
測試種類:
單元測試(Unit Test):
相關(guān)框架:Kiwi、Specta
?UI測試(UI Test):模擬用戶操作,進而從業(yè)務層面測試
相關(guān)框架:KIF、appium
1.UI比較少則進行單元測試、UI測試
2.UI比較多則進行單元測試,UI測試根據(jù)場景決定
單元測試特性
單元性(快速)
測試力度足夠小,能夠精確定位問題
單一職責:一個測試case只負責一條路徑,測試代碼中不允許有復雜的邏輯條件
獨立性(無依賴)
避免單元測試之間的依賴關(guān)系,一個測試的運行不依賴于其他測試代碼的運行結(jié)果
可重復性(冪等性)
每次執(zhí)行的結(jié)果都相同
自驗證
不靠人來檢查,必須使用斷言
盡可能斷言具體的內(nèi)容(簡單的為空判斷起不到太大的作用)
測試代碼必須有好的前置條件和后置斷言
CocoaPods初始化工程推薦的框架為Kiwi、Specta
優(yōu)點缺點備注
OCMock1、語法與OC類似
2、文檔全面
3、學習成本低
1、3.8.1只支持模擬器
2、需要使用<=3.7.1版本
>=3.8.1不支持真機
建議使用<= 3.7.1版本
Specta1、輕松量級、功能簡單、鏈式語法1、文檔少、學習成本高
2、如果需要Mock、Stub功能需要引入OCMock
Kiwi1、功能豐富
2、內(nèi)置Mock、驗證等功能
1、文檔較少、學習成本較高
2、重量級
XCTestOCMockExpecta
AFNetworking?
SDWebImage??
FMDB??
MJExtension?
Pop??
靈活性學習成本遷移成本改造成本
XCTest高低低低
Specta低高高高
Kiwi低高高高
3.1 根據(jù)開源框架使用單元測試情況使用最多的是XCTest
3.2 XCTest配合OCMock、Expecta可以實現(xiàn)Kiwi/Specta功能
暫無UI測試,后續(xù)補充
1.創(chuàng)建新工程請選中 ??Include Tests
2.如果老工程改造File->New->Target->iOS->Unit Testing Bundle
3.集成OCMock: pod 'OCMock', '3.7.1' 使用<=3.7.1版本(>=3.8.1不支持真機) 參考地址:https://ocmock.org/ios/
4.集成Expecta: pod 'Expecta'
```
// person.h
@interface Person : NSObject
- (void)eat;
- (NSInteger)sleep;
@end
// person.m
@interface Person ()
@property(nonatomic, assign) NSInteger age;
@end
@implementation Person
- (void)eat {?
? ? NSLog(@"eat");
}
- (NSInteger)sleep {?
? ? NSLog(@"sleep");?
? ? return 8;
}
- (void)run {???
? ? NSLog(@"Person Run");
}
@end
```
一個測試文件只能測試一個類相關(guān)功能
測試類以Test結(jié)尾,例如:AFImageDownloaderTests
測試方法以Test開頭,做到見名知意,例如: testNilProgressDoesNotCauseCrash
寫單元測試時不要修改項目源碼
每個case只測試一種情況,提高代碼可讀性,目標測試case中盡量減少if…else…, 如果if…else…太多是不是需要重構(gòu)代碼。
例: Person有兩個方法eat和sleep要驗證,每個case單獨驗證一個方法
```
// case1
- (void)testPersonEat {???
? ? Person *person = [[Person alloc] init];???
? ? [person eat];
}?
// case2
- (void)testPersonSleep {???
? ? Person *person = [[Person alloc] init];???
? ? NSInteger tiem = [person sleep]; ????
? ? XCTAssertEqual(time, 6);
}
```
每個case分為三步: 1.mock對象,準備測試數(shù)據(jù) 2.調(diào)用目標API 3.驗證輸出和行為
```
- (void)testPersonSleep {???????
? ? // Given???
? ? Person *person = [[Person alloc] init];???
? ? // when???
? ? NSInteger tiem = [person sleep]; ????
? ? // then???
? ? XCTAssertEqual(time, 6);
}
```
當一個case想要訪問私有方法或者私有屬性時就需要被訪問類公開屬性或方法,我們要避免訪問私有屬性或方法,在迫不得已情況下可以通過在測試文件開頭用一個category來暴露被訪問類的私有屬性或者方法。
訪問person中的私有方法run和私有屬性age,可以參考一下代碼
```
// person.h
@interface Person : NSObject
@end?
// person.m
@interface Person ()
@property(nonatomic, assign) NSInteger age;
@end
@implementation Person
- (void)run {???
? ? NSLog(@"Person Run");
}
@end
// UITestDemoTests.m?
@interface Person (UITestDemoTests)
- (void)run;
@property(nonatomic, assign) NSInteger age;
@end?
@interface UITestDemoTests : XCTestCase
- (void)testPrivateProperty {???
? ? Person *person = [[Person alloc] init];???
? ? person.age = 100; ???
? ? XCTAssertEqual(person.age, 100);
}?
- (void)testPrivateMethod {???
? ? Person *person = [[Person alloc] init];???
? ? [person run];
}
@end
```
【強制】代碼編寫前做好代碼分層、隔離設計,為后續(xù)單元測試做準備。
【強制】核心應用核心業(yè)務增量代碼一定要寫單元測試。
【強制】保持單元測試的獨立性。為了保證單元測試穩(wěn)定可靠且便于維護,單元測試用例之 間決不能互相調(diào)用,也不能依賴執(zhí)行的先后次序。 反例:method2 需要依賴 method1 的執(zhí)行,將執(zhí)行結(jié)果作為 method2 的輸入
【強制】對于單元測試,要保證測試粒度足夠小,有助于精確定位問題。單測粒度至多是類 級別,一般是方法級別。 說明:只有測試粒度小才能在出錯時盡快定位到出錯位置。單測不負責檢查跨類或者跨系統(tǒng)的交互邏輯, 那是集成測試的領(lǐng)域。
【強制】暴露給外部使用的API必須進行單元測試且通過。 說明:新增代碼及時補充單元測試,如果新增代碼影響了原有單元測試,請及時修正。
【強制】單元測試代碼不允許寫在業(yè)務代碼目錄下。
【強制】單元測試類的命名應該和被測試類保持一致xxxTest,測試方法為被測方法名textxxx。
【強制】開發(fā)必須保證自己寫的單元測試能在本地執(zhí)行通過,才能提交。
【強制】重視邊界值測試,充分考慮邊界和異常,核心業(yè)務模塊的代碼保證盡量高的代碼測試覆蓋率。
【強制】不需要的單元測試直接刪除,不要注釋掉,如果非要注釋請寫清楚注釋理由。
【強制】對于外部第三方SDK要單獨進行封裝隔離,方便后續(xù)單元測試和更換。
【推薦】對于依賴本應用之外的所有第三方環(huán)境的單元測試,建議使用Mock的方式進行測試,盡量將正常/異常情況模擬到,即做到盡可能的擺脫對環(huán)境依賴、持續(xù)重復運行。
【推薦】寫測試代碼的目的是為了提高業(yè)務代碼質(zhì)量,嚴禁為達到測試要求而書寫不規(guī)范測試代碼;對于不可測的代碼建議做必要的重構(gòu),使代碼變的可測。
【推薦】編寫單元測試代碼遵守 BCDE 原則,以保證被測試模塊的交付質(zhì)量。
? B:Border,邊界值測試,包括循環(huán)邊界、特殊取值、特殊時間點、數(shù)據(jù)順序等。??
? C:Correct,正確的輸入,并得到預期的結(jié)果。??
? D:Design,與設計文檔相結(jié)合,來編寫單元測試。??
? E:Error,強制錯誤信息輸入(如:非法數(shù)據(jù)、異常流程、業(yè)務允許外等),并得到預期的結(jié)果。
【推薦】對于不可測的代碼在適當?shù)臅r機做必要的重構(gòu),使代碼變得可測,避免為了達到測 試要求而書寫不規(guī)范測試代碼。
【推薦】單元測試作為一種質(zhì)量保障手段,在項目提測前完成單元測試,不建議項目發(fā)布后 補充單元測試用例。
【推薦】所有請求服務端的接口全部單獨封裝做到可支持單元測試針對測試。
理想:覆蓋所有類、函數(shù)、邏輯
實際:至少保證暴露給外部的類和邏輯應該測試到
測試重點:
盡量避免測試類的私有成員/方法,它讓測試變得繁瑣而且更難維護。
如果有私有成員確實需要進行直接測試,可以考慮把它重構(gòu)到工具類的公有方法中,但要注意這么做是為了改善設計, 而不是幫助測試.
接口較為復雜,含有較多的邏輯分支時,單元測試應盡可能測試到所有的業(yè)務邏輯
對于每個Service都要進行單元測試
單元測試Mock數(shù)據(jù)要模擬正常/異常情況,客戶端各種可能出現(xiàn)的情況都要覆蓋到
蘋果相關(guān)權(quán)限如:網(wǎng)絡、定位、通知等
單元測試要模擬已/未獲取相關(guān)權(quán)限
變量是否有初始值或在某些場景下是否有默認值
變量、數(shù)組、字典等的溢出越界等
注意邊界和異常值
變量無賦值(null)
變量是數(shù)值或字符
主要邊界:最大值,最小值,無窮大
溢出邊界:在邊界外面取值+/-1
臨近邊界:在邊界值之內(nèi)取值+/-1
字符串的設置,空字符串
目標集合的類型和應用邊界
變量是規(guī)律的,測試無窮大的極限,無窮小的極限
數(shù)組越界
字典設置空值
多線程操作同一變量
對于涉及到業(yè)務邏輯的異常,需要覆蓋
對于封裝第三方的SDK雖然沒有暴露給外部,但要保證第三方SDK所有場景覆蓋
對于有些場景可能在弱網(wǎng)下出問題,需要模擬弱網(wǎng)情況
對于前后臺切換可能影響業(yè)務的場景要覆蓋到
有外部數(shù)據(jù)依賴時盡量使用Mock進行數(shù)據(jù)模擬
公有接口
重要復雜的算法、邏輯、功能以及容易出錯的分支
請求服務端的接口
蘋果相關(guān)權(quán)限未/開啟情況下
局部數(shù)據(jù)結(jié)構(gòu)測試
邊界條件測試
正常/異常情況
所有獨立代碼測試
弱網(wǎng)測試
前后臺切換測試
單例相關(guān)測試
有外部依賴時使用Mock
后續(xù)補充
TDD:在開發(fā)功能代碼之前,先編寫單元測試用例代碼,測試代碼驅(qū)動產(chǎn)品代碼開發(fā)
邊開發(fā)邊單測:每個功能點,先寫產(chǎn)品代碼,再寫單測代碼
先開發(fā)再單測:先完成所有或部分功能點,在提測前補單測
TDD做好比較難,存在爭議也比較多;網(wǎng)絡上推薦建議采用邊開發(fā)邊單測的方式。
但是客戶端通常采用先開發(fā)再單元測試。
注意不要導致內(nèi)存泄漏,可使用XCode或者MLeaksFinder自行檢測
對于復雜UI需要檢測幀率是否達標,檢測設備以6s設備為準,不允許使用6s以上設備檢測
6s性能較差(檢測設備可以根據(jù)設備分布情況調(diào)整)
暫時不考慮
暫時不考慮
如果頁面幀率不達標請使用XCode檢測渲染卡頓問題
如果頁面需要請求多個接口,請對頁面加載時間進行單元測試,如果不達可能需要考慮重構(gòu)頁面或接口
針對復雜頁面請對加載時間進行單元測試
所有請求網(wǎng)絡的接口都要進行單元測試,以保證接口正確性和穩(wěn)定性。
對于有些接口可能依賴外部參數(shù)或者數(shù)據(jù),請自行創(chuàng)造測試條件。
暫時不考慮
代碼覆蓋率統(tǒng)計是用來發(fā)現(xiàn)沒有被測試覆蓋的代碼;代碼覆蓋率統(tǒng)計不能完全用來衡量代碼質(zhì)量。
分析微覆蓋部分的代碼,從而反推在前期測試設計是否充分,沒有覆蓋到的代碼是否是測試的盲點,為什么沒有考慮到?需求/設計不夠清晰,測試設計的理解有誤,工程方法應用后的造成的策略性放棄等等,之后進行補充測試用例設計。
檢測出程序中的廢代碼,可以逆向反推在代碼設計中思維混亂點,提醒設計/開發(fā)人員理清代碼邏輯關(guān)系,提升代碼質(zhì)量。
代碼覆蓋率高不能說明代碼質(zhì)量高,但是反過來看,代碼覆蓋率低,代碼質(zhì)量不會高到哪里去,可以作為測試自我審視的重要工具之一。
Xcode為我們提供覆蓋檢測, 使用流程
Edit Scheme - Test - Options - 勾選 Gather coverage for (全部文件, 或者自定義部分文件)
然后command+u 跑測試用例之后便可在如圖所示,查閱單元測試的覆蓋率
```
XCTFail(format…) 生成一個失敗的測試;
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是 C語言標量、結(jié)構(gòu)體或聯(lián)合體時使用, 判斷的是變量的地址,如果地址相同則返回TRUE,否則返回NO);
XCTAssertNotEqual(a1, a2, format...)判斷不等(當a1和a2是 C語言標量、結(jié)構(gòu)體或聯(lián)合體時使用);
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)判斷相等,(double或float類型)提供一個誤差范圍,當在誤差范圍(+/-accuracy)以內(nèi)相等時通過測試; XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...) 判斷不等,(double或float類型)提供一個誤差范圍,當在誤差范圍以內(nèi)不等時通過測試;
XCTAssertThrows(expression, format...)異常測試,當expression發(fā)生異常時通過;反之不通過;(很變態(tài))
XCTAssertThrowsSpecific(expression, specificException, format...) 異常測試,當expression發(fā)生specificException異常時通過;反之發(fā)生其他異?;虿话l(fā)生異常均不通過; XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)異常測試,當expression發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過; XCTAssertNoThrow(expression, format…)異常測試,當expression沒有發(fā)生異常時通過測試;
XCTAssertNoThrowSpecific(expression, specificException, format...)異常測試,當expression沒有發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過; XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)異常測試,當expression沒有發(fā)生具體異常、具體異常名稱的異常時通過測試,反之不通過
```
What Mock?
Mock就是做一個假的object,對這個object里的方法的調(diào)用,都會被Mockito攔截,然后返回用戶預設的行為。這樣可以繞過需要從其它地方拿數(shù)據(jù)的地方,直接返回用戶預設的數(shù)據(jù),進行單元測試。
When mock?
1.其它的協(xié)同模塊尚未開發(fā)完成
2.被測試模塊需要和一些不容易構(gòu)造、比較復雜的對象進行交互
3.由于不能肯定其它模塊的正確性,我們也無法確定測試中發(fā)現(xiàn)的問題是由哪個模塊引起的
4.網(wǎng)絡交互,外部系統(tǒng)調(diào)用接口,如果兩個被測模塊之間是通過網(wǎng)絡進行交互的
Why use mock?
我們可以使用蘋果的Runtime來實現(xiàn)Mock,但是需要自己開發(fā)太多基礎(chǔ)東西,所以引入Mock框架
Mock: 創(chuàng)建一個模擬對象實例,我們可以驗證,修改它的行為
Stub: Mock對象的函數(shù)返回特定的值
Partial Mock: 重寫Mock對象的方法
需要注意的是mock的對象在用例結(jié)束后要stopMocking,避免由于單例或者property導致的用例之間相互影響
```
id classMock = OCMClassMock([SomeClass class]);
```
Creates a mock object that can be used as if it were an instance of SomeClass. It is possible to mock instance and class methods defined in the class and its superclasses.
There are some subtleties when mocking class methods. See mocking class methods below.
??例子:
```
id mock = OCMClassMock([NSString class]);
```
```
id protocolMock = OCMProtocolMock(@protocol(SomeProtocol));
```
Creates a mock object that can be used as if it were an instance of an object that implements SomeProtocol. Otherwise they work like class mocks.
??例子:
```
// 輔助類
@protocol TestProtocol
+ (NSString *)stringValueClassMethod;
- (int)primitiveValue;
@optional
- (id)objectValue;
- (void)voidWithArgument:(id)argument;
@end
// demo1
- (void)testProtocolClassMethod {???
? ? id mock = OCMProtocolMock(@protocol(TestProtocol));???
? ? OCMStub([mock stringValueClassMethod]).andReturn(@"stubbed");???
? ? id result = [mock stringValueClassMethod];???
? ? XCTAssertEqual(@"stubbed", result, @"Should have stubbed the class method.");
}
// demo2
- (void)testRefusesToCreateProtocolMockForNilProtocol {???
? ? XCTAssertThrows(OCMProtocolMock(nil));
}
// demo3
- (void)testArgumentsGetReleasedAfterStopMocking {???
? ? __weak id weakArgument;???
? ? id mock = OCMProtocolMock(@protocol(TestProtocol));???
? ? @autoreleasepool {???????
? ? ? ? NSObject *argument = [NSObject new];???????
? ? ? ? weakArgument = argument;???????
? ? ? ? [mock voidWithArgument:argument];???????
? ? ? ? [mock stopMocking];???
? ? }???
? ? XCTAssertNil(weakArgument);
}
```
1.3 Strict class and protocol mocks
```
id classMock = OCMStrictClassMock([SomeClass class]);
id protocolMock = OCMStrictProtocolMock(@protocol(SomeProtocol));
```
Creates a mock object in strict mode. By default mocks are nice, they return nil (or the correct default value for the return type) for whatever method is called. In contrast, strict mocks raise an exception when they receive a method that was not explicitly expected. See strict mocks and expectations below.
??例子:
```
// demo1
- (void)testSetsUpStubsForCorrectMethods {???
? ? id mock = OCMStrictClassMock([NSString class]);???
? ? OCMStub([mock uppercaseString]).andReturn(@"TEST_STRING");???
? ? XCTAssertEqualObjects(@"TEST_STRING", [mock uppercaseString], @"Should have returned stubbed value");???
? ? XCTAssertThrows([mock lowercaseString]);
}
// demo2
- (void)testSetsUpStubsWithNonObjectReturnValues {???
? ? id mock = OCMStrictClassMock([NSString class]);???
? ? OCMStub([mock boolValue]).andReturn(YES);???
? ? XCTAssertEqual(YES, [mock boolValue], @"Should have returned stubbed value");
}
// demo3
- (void)testCanUseVariablesInInvocationSpec {???
? ? id mock = OCMStrictClassMock([NSString class]);???
? ? NSString *expected = @"foo";???
? ? OCMStub([mock rangeOfString:expected]).andReturn(NSMakeRange(0, 3));???
? ? XCTAssertThrows([mock rangeOfString:@"bar"], @"Should not have accepted invocation with non-matching arg.");
}
// demo4
- (void)testCanUseMacroToStubMethodWithAnyNonObjectArgument {???
? ? id mock = OCMStrictClassMock([NSString class]);???
? ? OCMStub([mock commonPrefixWithString:@"foo" options:0]).ignoringNonObjectArgs();???
? ? XCTAssertNoThrow([mock commonPrefixWithString:@"foo" options:NSCaseInsensitiveSearch]);
}
```
`id partialMock = OCMPartialMock(anObject);`
Creates a mock object that can be used in the same way as anObject. Any method that is not stubbed is forwarded to anObject. When a method is stubbed and that method is invoked using a reference to the real object, the mock will still be able to handle the invocation. Similarly, when a method is invoked using a reference to anObject, rather than the mock, it can still be verified later.
There are some subtleties when using partial mocks. Seepartial mocksbelow.
??例子:
```
// demo1
- (void)testSetsUpStubReturningNilForIdReturnType {???
? ? id mock = OCMPartialMock([NSArray arrayWithObject:@"Foo"]);???
? ? OCMStub([mock lastObject]).andReturn(nil);???
? ? XCTAssertNil([mock lastObject], @"Should have returned stubbed value");
}
// demo2
@interface TestClassWithClassReturnMethod : NSObject
- (Class)method;
@end?
@implementation TestClassWithClassReturnMethod
- (Class)method {???
? ? return [self class];
}
@end
// demo2
- (void)testSetsUpStubReturningNilForClassReturnType {???
? ? id mock = OCMPartialMock([[TestClassWithClassReturnMethod alloc] init]);??? ????
? ? OCMStub([mock method]).andReturn(Nil);???
? ? XCTAssertNil([mock method], @"Should have returned stubbed value");???
? ? // sometimes nil is used where Nil should be used???
? ? OCMStub([mock method]).andReturn(nil);???
? ? XCTAssertNil([mock method], @"Should have returned stubbed value");
}
```
Observer mocks are deprecated as of OCMock 3.8. Please use XCTNSNotificationExpectation instead.
`id observerMock = OCMObserverMock();`
Creates a mock object that can be used to observe notifications. The mock must be registered in order to receive notifications. Seeobserver mocksbelow for details.
??例子:
```
// 輔助類
@protocol TestProtocolForMacroTesting
- (NSString *)stringValue;
@end
// demo1
- (void)testSetsUpNotificationPostingAndNotificationObserving {???
? ? id mock = OCMProtocolMock(@protocol(TestProtocolForMacroTesting));???
? ? NSNotification *n = [NSNotification notificationWithName:@"TestNotification" object:nil];???
? ? id observer = OCMObserverMock();???
? ? [[NSNotificationCenter defaultCenter] addMockObserver:observer name:[n name] object:nil];???
? ? OCMExpect([observer notificationWithName:[n name] object:[OCMArg any]]);???
? ? OCMStub([mock stringValue]).andPost(n);???
? ? [mock stringValue];???
? ? OCMVerifyAll(observer);
}
// demo2
- (void)testNotificationObservingWithUserInfo {???
? ? id observer = OCMObserverMock();???
? ? [[NSNotificationCenter defaultCenter] addMockObserver:observer name:@"TestNotificationWithInfo" object:nil];???
? ? OCMExpect([observer notificationWithName:@"TestNotificationWithInfo" object:[OCMArg any] userInfo:[OCMArg any]]);????
? ? [[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotificationWithInfo" object:self userInfo:@{ @"foo": @"bar" }];????
? ? OCMVerifyAll(observer);
}
```
Why use Expecta?
XCTest中的斷言可能不能滿足日常開發(fā)需求,所以引入斷言框架,不強制使用請根據(jù)需要自行決定
......
1.https://juejin.cn/post/6844904138388537352#heading-9
2.https://github.com/zhuifengshen/SwiftUnitTestsSamples
3.https://github.com/cyneck/unit-test-specification
4.https://www.cnblogs.com/M-Silencer/p/11215065.html