模塊功能需求
1钥平,從上一個頁面实撒,點擊一個按鈕,push進入模塊控制器帖池。
2奈惑,控制器執(zhí)行viewDidLoad后,開始加載接口數(shù)據(jù)睡汹。
3肴甸,請求不到數(shù)據(jù),需要有無數(shù)據(jù)提示囚巴。
4原在,請求到數(shù)據(jù),則展示列表彤叉。
5庶柿,列表有三種數(shù)據(jù)類型,A,B,C, 形式一樣秽浇,顯示一張圖片浮庐,和一個標題。同一種數(shù)據(jù)類型柬焕,圖片一樣审残,不同數(shù)據(jù)類型圖片不一樣,標題是隨意的斑举。
5搅轿,點擊列表,根據(jù)數(shù)據(jù)類型富玷,跳轉(zhuǎn)到不同頁面璧坟。
這是很常見的模塊既穆,現(xiàn)在嘗試用TDD的方式去實現(xiàn)它。我們暫且先采用MVC的架構去開發(fā)雀鹃,那么要有一個Model類去承接和轉(zhuǎn)換接口數(shù)據(jù)幻工;要有一個TableView去展示數(shù)據(jù);要有一個Controller去負責請求數(shù)據(jù)褐澎、封裝數(shù)據(jù)和提供數(shù)據(jù)給TableView去展示会钝。
嘗試去開發(fā)Model類
TDD講究以測試驅(qū)動開發(fā)伐蒋,因此寫測試用例先于寫產(chǎn)品代碼工三。這時候的測試用例可以為我們描述需求。限于篇幅先鱼,我這里盡量只寫幾個我認為重要的測試用例俭正,測試用例寫得越多、覆蓋得越廣其實越好焙畔,但誰讓我們總是時間有限掸读、精力有限呢。我們的測試要盡量覆蓋到我們上面提到的幾點需求宏多,其中需求【5】的一部分可以通過測試Model來覆蓋儿惫,那就是不同類型數(shù)據(jù)對應不同圖片,我們要確保當Model是A,B,C類型時伸但,分別對應圖片A,B,C肾请。
【tc 1.1,測試A類型數(shù)據(jù)對應A類型圖標】
- (void)testTypeAModelHasAPictureUrl{
MyModel *model = [[MyModel alloc] init];
model.type = ModelTypeA;
NSString *picAUrl = @"AUrl";
XCTAssertTrue([model.picUrl isEqualToString:picAUrl]);
}
我們得到了第一個測試用例更胖,從它身上我們可以了解到:1铛铁,測試用例名字最好寫得見名知意,因此却妨,測試用例的名字可能比較長饵逐,反正如果想少寫些注釋,就讓方法名來說明測試意圖吧彪标。通常我的習慣是倍权,用例名稱包含測了什么、期望是什么這兩部分內(nèi)容捞烟。2薄声,只要能夠保證被測邏輯是正確的,其他的怎么荒謬都無所謂坷襟。你看到這個測試用例的picAUrl是什么了嗎奸柬?它不是一個有效的Url,但是有什么關系呢婴程,這里我們不是測試它的正確性廓奕,我們測的是當model的type是ModelTypeA時,model的picUrl應該是對應著某個字符串。3桌粉,一個失敗的測試用例也是很有用的蒸绩,它起碼能夠說明某個需求或功能沒有開發(fā)。其實铃肯,寫完這個測試用例后患亿,我的xcode是這樣的:
它甚至不能編譯通過,因為押逼,我現(xiàn)在還沒有定義MyModel這個類步藕!
但是,我們已經(jīng)做了一件很有意義的事情了挑格,那就是我們寫了一個失敗的測試用例咙冗。這就是TDD的Red-Green-Refactor流程里面的第一個階段,Red階段∑現(xiàn)在我們要進入第二個Green階段雾消,我們要寫我們的產(chǎn)品代碼,讓這個失敗的測試用例有失敗變成通過挫望,即由Red變成Green立润。
MyModel代碼:
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, ModelType){
ModelTypeA = 0,
ModelTypeB,
ModelTypeC
};
@interface MyModel : NSObject
@property (nonatomic, assign) ModelType type;
@property (nonatomic, copy) NSString *picUrl;
@end
#import "MyModel.h"
@implementation MyModel
- (NSString *)picUrl{
if (self.type == ModelTypeA) {
return @"AUrl";
}
return nil;
}
@end
產(chǎn)品代碼終于可以讓【tc 1.1】通過了,即讓它變成Green媳板。單靠這個測試用例桑腮,還不足以覆蓋完全需求【5】的圖片對應數(shù)據(jù)類型的需求。因為拷肌,還有B,C兩種類型沒測呢到旦,好,我們接下來追加更多的測試用例:
【tc 1.2巨缘,tc 1.3添忘,tc 1.4】
- (void)testTypeBModelHasBPictureUrl{
MyModel *model = [[MyModel alloc] init];
model.type = ModelTypeB;
NSString *picBUrl = @"BUrl";
XCTAssertTrue([model.picUrl isEqualToString:picBUrl]);
}
- (void)testTypeCModelHasCPictureUrl{
MyModel *model = [[MyModel alloc] init];
model.type = ModelTypeC;
NSString *picCUrl = @"CUrl";
XCTAssertTrue([model.picUrl isEqualToString:picCUrl]);
}
- (void)testAPicUrlBPicUrlCPicUrlAreNotEqualToEachOther{
MyModel *model = [[MyModel alloc] init];
model.type = ModelTypeA;
NSString *picAUrl = model.picUrl;
model.type = ModelTypeB;
NSString *picBUrl = model.picUrl;
model.type = ModelTypeC;
NSString *picCUrl = model.picUrl;
XCTAssertFalse([picAUrl isEqualToString:picBUrl]);
XCTAssertFalse([picAUrl isEqualToString:picCUrl]);
XCTAssertFalse([picBUrl isEqualToString:picCUrl]);
}
然后,先執(zhí)行它們:
發(fā)現(xiàn)了一些有趣的情況若锁。我們當然知道搁骑,第一個測試用例的成功,是由于我們我們實現(xiàn)了它要求的功能又固,第二仲器、三個測試用例的失敗是必然的,因為我們沒有去實現(xiàn)它們的相應功能仰冠,而它們的失敗提醒著我們有待完成的工作乏冀。關鍵是第四個測試用例居然通過了,而我們并沒有針對它做相應的編碼洋只。這其實告訴我們辆沦,我們的測試有漏洞昼捍,需要完善,因為當model.picUrl都為nil時肢扯,第四個測試用例是可以通過的妒茬,但這不是我們想要的結(jié)果。所以蔚晨,我們再補充一個測試用例:
【tc 1.5】
- (void)testAPicUrlBPicUrlCPicUrlAreNotEqualToNil{
MyModel *model = [[MyModel alloc] init];
model.type = ModelTypeA;
XCTAssertNotNil(model.picUrl);
model.type = ModelTypeB;
XCTAssertNotNil(model.picUrl);
model.type = ModelTypeC;
XCTAssertNotNil(model.picUrl);
}
再執(zhí)行所有測試:
這樣我們就放心了乍钻,因為【tc 1.5】是【tc 1.4】的漏洞的補充,只要【tc 1.4】和【tc 1.5】都通過就沒問題铭腕。
下面银择,我們執(zhí)行Green階段,讓以上失敗的測試用例都通過谨履,MyModel.m的代碼:
#import "MyModel.h"
@implementation MyModel
- (NSString *)picUrl{
switch (self.type) {
case ModelTypeA:
return @"AUrl";
break;
case ModelTypeB:
return @"BUrl";
break;
case ModelTypeC:
return @"CUrl";
break;
default:
return nil;
break;
}
}
@end
注意到欢摄,現(xiàn)在為止,我們已經(jīng)執(zhí)行了兩次Ren-Green流程笋粟,為什么我們還沒有執(zhí)行一次Red-Green-Refactor的完整流程呢?因為第三個流程Refator要看情況的析蝴,在沒有必要重構代碼時害捕,我們當然就不會去重構,所以也就不會有Refactor階段出現(xiàn)闷畸,比如我們寫完【tc 1.1】的產(chǎn)品代碼尝盼,然后跑過了它后,就沒有需要重構的代碼佑菩,所以我們的第一個流程止于Red-Green盾沫,并沒有達到Red-Green-Refactor。所以實踐中殿漠,我發(fā)現(xiàn)通常是執(zhí)行了好幾次Red-Green流程后赴精,才會執(zhí)行一次Red-Green-Refactor流程,比如現(xiàn)在就是執(zhí)行Refactor的時候了绞幌。Refactor流程既重構產(chǎn)品代碼蕾哟,也會去重構測試代碼。我們現(xiàn)在的測試代碼有了一些冗余代碼需要提取重用莲蜘,那就是MyModel的初始化谭确,反正每個tc都用到,我們就把這部分代碼挪到setUp方法里面去票渠。
重構后的測試代碼:
#import <XCTest/XCTest.h>
#import "MyModel.h"
@interface MyModelTests : XCTestCase
@property (nonatomic, strong) MyModel *model;
@end
@implementation MyModelTests
- (void)setUp {
[super setUp];
self.model = [[MyModel alloc] init];
}
- (void)tearDown {
self.model = nil;
[super tearDown];
}
- (void)testTypeAModelHasAPictureUrl{
self.model.type = ModelTypeA;
NSString *picAUrl = @"AUrl";
XCTAssertTrue([self.model.picUrl isEqualToString:picAUrl]);
}
- (void)testTypeBModelHasBPictureUrl{
self.model.type = ModelTypeB;
NSString *picBUrl = @"BUrl";
XCTAssertTrue([self.model.picUrl isEqualToString:picBUrl]);
}
- (void)testTypeCModelHasCPictureUrl{
self.model.type = ModelTypeC;
NSString *picCUrl = @"CUrl";
XCTAssertTrue([self.model.picUrl isEqualToString:picCUrl]);
}
- (void)testAPicUrlBPicUrlCPicUrlAreNotEqualToEachOther{
self.model.type = ModelTypeA;
NSString *picAUrl = self.model.picUrl;
self.model.type = ModelTypeB;
NSString *picBUrl = self.model.picUrl;
self.model.type = ModelTypeC;
NSString *picCUrl = self.model.picUrl;
XCTAssertFalse([picAUrl isEqualToString:picBUrl]);
XCTAssertFalse([picAUrl isEqualToString:picCUrl]);
XCTAssertFalse([picBUrl isEqualToString:picCUrl]);
}
- (void)testAPicUrlBPicUrlCPicUrlAreNotEqualToNil{
self.model.type = ModelTypeA;
XCTAssertNotNil(self.model.picUrl);
self.model.type = ModelTypeB;
XCTAssertNotNil(self.model.picUrl);
self.model.type = ModelTypeC;
XCTAssertNotNil(self.model.picUrl);
}
@end
重構完成后逐哈,記得全部運行一次測試用例,保證它們繼續(xù)是通過的问顷。
重構代碼有時候是會上癮的昂秃,根本停不下來薯鼠。
當我們的測試用例一多了之后,我們可能還會去思考如果更好地組織它們械蹋,讓它們更好被管理和使用出皇。比如上面的【tc 1.1,tc 1.2, tc 1.3】 能不能合并成下面的【tc 1.6】呢哗戈,這樣測試用例的數(shù)量就少了下來郊艘,代碼也少了下來,能為我們減少一些管理壓力而測試覆蓋率還跟原來一樣唯咬。
【tc 1.6】
- (void)testTypeATypeBTypeCModelAllHasTheirOwnPicUrl{
self.model.type = ModelTypeA;
XCTAssertTrue([self.model.picUrl isEqualToString:@"AUrl"]);
self.model.type = ModelTypeB;
XCTAssertTrue([self.model.picUrl isEqualToString:@"BUrl"]);
self.model.type = ModelTypeC;
XCTAssertTrue([self.model.picUrl isEqualToString:@"CUrl"]);
}
我是不建議這種重構的纱注,原因是它破壞了測試用例的單一功能原則。好的測試用例只測一個單一小功能胆胰,為什么要強調(diào)這種原則呢狞贱,因為當一個測試用例失敗時,它應該讓你迅速定位到出錯的代碼蜀涨,這就是測試用例的又一個重要功能瞎嬉,那就是測試用例應當能夠顯著地減少我們?nèi)ebug的時間。
如果用【tc 1.6】去代替【tc 1.1厚柳,tc 1.2氧枣,tc 1.3】,那么MyModel.m的下面幾種代碼的修改都會讓【tc 1.6】失敗别垮。
情況一:
- (NSString *)picUrl{
switch (self.type) {
case ModelTypeA:
return @"AUrl";
break;
case ModelTypeB:
return @"AUrl";
break;
case ModelTypeC:
return @"CUrl";
break;
default:
return nil;
break;
}
}
情況二:
- (NSString *)picUrl{
switch (self.type) {
case ModelTypeA:
return @"AUrl";
break;
case ModelTypeB:
return @"BUrl";
break;
case ModelTypeC:
return nil;
break;
default:
return nil;
break;
}
}
情況三:
- (NSString *)picUrl{
switch (self.type) {
case ModelTypeA:
return @"CUrl";
break;
case ModelTypeB:
return @"BUrl";
break;
case ModelTypeC:
return @"CUrl";
break;
default:
return nil;
break;
}
}
每次出錯便监,我們都得查看出錯的測試用例代碼才知道產(chǎn)品代碼出錯的地方,如果不用統(tǒng)一集成的這個測試用例碳想,仍然用我們一開始分散的測試用例烧董。由于分散的測試用例的測試粒度是switch分支級別的,比粒度是方法的集中測試用例粒度更小胧奔,因此逊移,情況一只會導致【tc 1.2】的失敗,情況二只會導致【tc 1.3】的失敗葡盗,情況三只會導致【tc 1.1】的失敗螟左。由于測試用例的名稱已經(jīng)將我們的測試定位和意圖表述的比較具體,我們就可以不怎么用進入到測試用例內(nèi)部去讀代碼觅够,就大概能猜測出產(chǎn)品代碼哪里出了問題胶背。根據(jù)測試用例快速定位出錯的代碼,也就自然而然的不需要我們花更多時間去debug源碼了喘先。
待續(xù)钳吟。。窘拯。红且。坝茎。