TDD
測試驅動開發(Test Driven Development,縮寫TDD),它的基本思想就是在開發功能代碼之前,先編寫測試代碼。也就是說在明確要開發某個功能后,首先思考如何對這個功能進行測試,并完成相應的測試用例,然后編寫相關代碼滿足這些測試,然后循環添加這些功能,直至開發結束。
TDD 的優點
- 開發完成即完工。傳統的編碼方式很難知道什么時候編碼結束了,TDD模式下開發人員可以明確自己的編碼工作已經結束了。
- 代碼大部分保持在高質量狀態。
- 減少文檔和代碼之間的差別。
開發過程:
- 明確當前要完成的功能。可以記錄成一個 TODO 列表。
- 快速完成針對此功能的測試用例編寫。
- 測試代碼編譯不通過。
- 編寫對應的功能代碼。
- 測試通過。
- 對代碼進行重構,并保證測試通過。
- 循環完成所有功能的開發。
第1次迭代 用戶場景
用戶場景
- 作為用戶,我要看到文章列表。
- 作為用戶,我要切換分類,看到不同的文章。
- 作為用戶,當我點擊更多時,我要看到更多的分類。
- 作為用戶,我可以刷新最新的數據。
- 作為用戶,我可以獲取更多的數據。
- 作為用戶,我要看到上方的輪播圖。
- 作為用戶,我點擊沒一個文章,要進入文章詳情。
- 作為用戶,我點擊輪播圖,要進入文章詳情。
初步構建模塊結構圖
TDD - Model
我們看到列表的每個條目都會顯示 title和 content。
那么我們就可以先編寫測試用例。
- (void)testInitial {
ArticalModel * artical = [ArticalModel new];
}
編譯,顯然不會通過,因為我們沒有 ArticalModel 這個類,所以在我們的項目的代碼里創建一個 ArticalModel 這個類。再編譯,通過。此時不管時我們的測試的代碼,還是項目的代碼,都沒有可以重構的。
接下來,我們看到列表里有title ,content, 日期和圖片,對應到我們ArticalModel 里應該也有。接下來編寫我們的測試用例。
- (void)testInitial_with_infomation {
ArticalModel * artical = [ArticalModel new];
artical.title = @"";
artical.name = @"";
artical.date = @"";
artical.imageUrl = @"";
XCTAssertNotNil(artical,"artical shouldNot be nil");
}
編譯,失敗。因為我們的Artical 里沒有title,name等屬性。好,那我們為 Artical 添加這些屬性。
@interface ArticalModel : NSObject
@property(nonatomic , strong) NSString* title;
@property(nonatomic , strong) NSString* name;
@property(nonatomic , strong) NSString* date;
@property(nonatomic , strong) NSString* imageUrl;
@end
ok,編譯通過,那看看我們的測試。測試通過了。
我們的最簡易的模型先到這里。接下來,管理模型的是ArticalManager ,新建 ArticalManagerTest 測試類,同樣測試初始化代碼。
- (void)testInitialManager{
ArticalManager * manager = [ArticalManager new];
XCTAssertNotNil(manager ,@"manager should not be nil");
}
編譯出錯,同樣,新建ArticalManager 類并導入。
articalManager 里是放 ArticalModel 的,所以接下來測試獲取所有 AricalModel。
- (void)testGetArticalsNotNil{
ArticalManager * manager = [ArticalManager new];
id articals = manager.articals;
XCTAssertNotNil(articals,@"articals should not be nil");
}
同樣,編譯報錯,因為我們的ArticalManager 里沒有articals屬性,我們給ArticalManager 加上artical 屬性。
@interface ArticalManager : NSObject
@property(nonatomic , strong) NSArray* articals;
@end
編譯成功,但是測試沒有通過,顯然,因為我們的articals 沒有初始化,默認為空。我們默認情況下應該返回一個空數組,再為ArticalManager 里實現 articals 的懶加載方法。
@implementation ArticalManager
-(NSArray*)articals{
if (!_articals) {
_articals = @[];
}
return _articals;
}
@end
再次編譯。測試通過,這時看看測試代碼發現有地方可以重構,兩個測試方法里都創建了ArticalManager 新的實例
- (void)testInitialManager{
ArticalManager * manager = [ArticalManager new];
XCTAssertNotNil(manager ,@"manager should not be nil");
}
- (void)testGetArticalsNotNil{
ArticalManager * manager = [ArticalManager new];
id articals = manager.articals;
XCTAssertNotNil(articals,@"articals should not be nil");
}
我們可以把 manager 抽出來,變為測試類的一個屬性,初始化方法放在 setUp 里,代碼就變成這樣
- (void)setUp {
[super setUp];
self.manager = [ArticalManager new];
}
- (void)testInitialManager{
XCTAssertNotNil(self.manager ,@"manager should not be nil");
}
- (void)testGetArticalsNotNil{
id articals = self.manager.articals;
XCTAssertNotNil(articals,@"articals should not be nil");
}
接下來,我們的ArticalManager 應該可以添加一個 文章的模型。
- (void)testAddAnArticalModel{
ArticalModel* artical = [ArticalModel new];
[self.manager addArticalModel:artical];
XCTAssertEqual(1,self.manager.articals.count,@"should have one artical");
}
編譯報錯。那我們為 ArticalManager 添加 addArticalModel 的接口,并完成它的實現。
-(void)addArticalModel:(ArticalModel*)artical{
[self.articals addObject : artical];
}
編譯報錯,self.articals 沒有addObject方法,因為articals 是 NSArray 類型的,我們把它定義成NSMutableArray再看看,修改如下
@property(nonatomic , strong) NSMutableArray* articals;
-(NSMutableArray*)articals{
if (!_articals) {
_articals =[ @[] mutableCopy ];
}
return _articals;
}
編譯,測試通過,可以正常添加ArticalModel。再取出來測試一下是不是我們添加的 ArticalModel
- (void)testAddAnArticalModel{
ArticalModel* artical = [ArticalModel new];
[self.manager addArticalModel:artical];
XCTAssertEqual(1,self.manager.articals.count,@"should have one artical");
artical.title = @"titleTest";
XCTAssertEqual(@"titleTest",[self.manager.articals[0] title],@"artical title should be equal");
}
果然測試通過,那要是我們隨便添加一個模型試試看
- (void)testAddIlegalModel{
NSObject* illegalModel = [NSObject new];
[self.manager addArticalModel: illegalModel];
XCTAssertEqual(1,self.manager.articals.count,@"should have one artical");
}
通過了,但這不應該是一個成功的測試,或者說上一步斷言編寫不正確,那我們修改測試用例,
- (void)testAddIlegalModel{
NSObject* illegalModel = [NSObject new];
[self.manager addArticalModel: illegalModel];
XCTAssertEqual(0,self.manager.articals.count,@"should have no articals");
}
測試不通過,因為非法的模型已經被加到 articals 數組里去了,然而,不是什么對象我們都可以給它添加進我們的ArticalManager 的,我們的ArticalManager 要為 TableView 服務,所以要嚴格控制 ArticalManager 數組內的元素。
如何控制呢,可以在添加前判斷一下被添加的對象是不是 Artical 類。但是這種代碼可擴展性低,將來如果tableView 需要顯示其它類型(比如 公告)的cell,而數據格式不完全一樣,公告就不該被加進來。
第二種方法可以以繼承的方式實現,比如我們把ArticalModel 作為基類,將來擴展的 BroadCastModel 可以繼承 ArticalModel ,因此在添加進 Articals 數組的時候我們直接使用基類的指針判讀類型。
第三種方法使用interface 的方式,對應于 OC 的protocal ,協議(接口)相比于類,耦合度更低。假設我還有一種模型 MessageModel 需要顯示,其數據模型和 BroadCastModel 相似 ,即需要被加進去 articals 數組,用第二種方法實現讓 MessageModel 繼承 BroadCastModel ,同樣可以達到需求 , 但是如果日后業務需求增加,需要更多的顯示種類,一昧的使用繼承的方式將導致整棵模型樹層級越來越深。假設有需求,需要改變你的層級樹中某一個Model 的屬性或者私有方法,那如果繼承了它的子類用到了該屬性或方法,就要相應的去修改。當我需要測試 MessageModel 時,需要縱向依賴BroadCastModel 和 ArticalModel ,一旦業務抽離,在另一個模塊或者項目中使用 MessageModel 時,需要將這里的繼承樹連根拔起。所以,如果你的繼承關系層級達到了三層或者更多,就應該停下來思考一下設計是否合理。 采用 interface 或 protocal 的方式,是橫向依賴,不管是ArticalModel 或者 MessageModel ,我要做tableView 的數據源,就要實現 TableViewModelCellProtocal 的方法, 這樣就能減少耦合度。其實這解決的是如何在 tableView 中顯示不同的數據格式的數據。
因為我們這邊只是測試的實例,我們就采用最簡單的第一種方法,在 addArticalModel 中添加如下代碼
if (![artical isKindOfClass:[ArticalModel class]]) {
return;
}
接下來,考慮到我們的數據是從網絡回來,肯定不是一條記錄一條記錄的添加,那我們就編寫批量數據的測試
- (void)testAddArticalArrToManager{
NSArray * articalArr = nil;
[self.manager.articals addArticalArrs : articalArr];
XCTAssertEqual(0,[self.manager.articals count],@"articals should be eqmpty");
}
編譯失敗,我們為 ArticalManager 添加該接口
-(void)addArticalArrs: (NSArray <NSObject * >* )aricalArr{
}
編譯測試均通過,接下來我們看一下添加一個真實的模型數組,
-(void)testAddRealArticalArrToManager{
ArticalModel * articalModel = [ArticalModel new];
articalModel.name = @"artical_one";
NSArray * articalArr = @[articalModel];
[self.manager addArticalArrs : articalArr];
XCTAssertEqual(1,[self.manager.articals count],@"articals should be eqmpty");
}
測試不通過,原因是addArticalArrs 方法里沒有實現,我們將添加它的實現如下,
[self.articals addObjectsFromArray: articalArr];
測試全都pass ,我們再看看我們添加的是不是正確的模型,添加如下代碼
XCTAssertEqual(articalModel.name , [self.manager.articals[0] name],@"they should be same name");
果然是同一個,測試通過,再試試添加非法的Array,我們設置的斷言是不應該被加入數組。
-(void)testAddIllegalArrToManager{
NSArray * articalArr = @[@"1",@"haha",@(2)];
[self.manager addArticalArrs : articalArr];
XCTAssertEqual(0,self.manager.articals.count,@"aricals should not be added");
}
測試失敗,結果是0 != 3 ,我們修改代碼來滿足我們的測試,我們希望它只要有一個元素不合格,就都不能插入。
-(void)addArticalArrs: (NSArray <ArticalModel *>* )articalArr{
__block BOOL illegal = false;
[articalArr enumerateObjectsUsingBlock:^(NSObject* obj, NSUInteger idx, BOOL *stop){
if (![obj isKindOfClass:[ArticalModel class]]) {
illegal = true;
*stop = YES;
}
}];
if (!illegal) {
[self.articals addObjectsFromArray: articalArr];
}
}
好了這次通過了,看看有沒有什么可以重構的,我們可以把判斷元素合格提取出來,將來如果需要更換判斷方式,比如采用協議來驗證,就可以很方便的修改
#pragma mark privateMethod
-(BOOL)isItemVaild:(NSObject*)obj{
return [artical isKindOfClass:[ArticalModel class]];
}
提取出私有方法,isItemValid ,現在ArticalManager 代碼如下
-(void)addArticalModel:(ArticalModel*)artical{
if([self isItemVaild: artical]){
[self.articals addObject:artical];
};
}
-(void)addArticalArrs: (NSArray <ArticalModel *>* )articalArr{
__block BOOL illegal = false;
[articalArr enumerateObjectsUsingBlock:^(NSObject* obj, NSUInteger idx, BOOL *stop){
if(![self isItemVaild: obj]){
illegal = YES,*stop = YES;
}
}];
if (!illegal) {
[self.articals addObjectsFromArray: articalArr];
}
}
#pragma mark privateMethod
-(BOOL)isItemVaild:(NSObject*)obj{
return [obj isKindOfClass:[ArticalModel class]];
}
接下來還有清空 artical 數組的測試己,編寫,過程是一樣的,假設該過程我們已經完成,現在回看我們的 ArticalManager 還有沒有什么可以修改的。
articals 作為 MSMutableArray 暴露在頭文件,這是相當危險的,也就是說可以讓外部的類隨意修改元素個數或articals 實例。所以我們將頭文件的 articals 改為外界只讀的。在ArticalManger.h 文件里聲明articals 為NSArray,并且只讀,.m 文件里使用 _articals 變量,該變量在對象創建時實例化。
.h
@property(nonatomic , strong , readonly) NSArray* articals;
.m
@interface ArticalManager(){
NSMutableArray* _articals;
}
@end
@implementation ArticalManager
-(instancetype)init{
_articals = @[].mutableCopy;
return self;
}
-(void)addArticalModel:(ArticalModel*)artical{
if([self isItemVaild: artical]){
[_articals addObject:artical];
};
}
...
大概 Model 層就是這樣子的,測試的思路大概也就是這樣,然而這些測試還只是冰山一角。完整寫下來,測試代碼大概是功能代碼的2~3 倍。