iOS嘗試用測試驅(qū)動的方法開發(fā)一個列表模塊【一】

模塊功能需求

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是這樣的:

image.png

它甚至不能編譯通過,因為押逼,我現(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í)行它們:

image.png

發(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í)行所有測試:


image.png

這樣我們就放心了乍钻,因為【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ù)钳吟。。窘拯。红且。坝茎。

demo:
https://github.com/zard0/TDDListModuleDemo.git

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市暇番,隨后出現(xiàn)的幾起案子嗤放,更是在濱河造成了極大的恐慌,老刑警劉巖壁酬,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件次酌,死亡現(xiàn)場離奇詭異,居然都是意外死亡舆乔,警方通過查閱死者的電腦和手機岳服,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來希俩,“玉大人吊宋,你說我怎么就攤上這事⊙瘴洌” “怎么了璃搜?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盒刚。 經(jīng)常有香客問我腺劣,道長,這世上最難降的妖魔是什么因块? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮籍铁,結(jié)果婚禮上涡上,老公的妹妹穿的比我還像新娘。我一直安慰自己拒名,他們只是感情好吩愧,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著增显,像睡著了一般雁佳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上同云,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天糖权,我揣著相機與錄音,去河邊找鬼炸站。 笑死星澳,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的旱易。 我是一名探鬼主播禁偎,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼腿堤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了如暖?” 一聲冷哼從身側(cè)響起笆檀,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盒至,沒想到半個月后酗洒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡妄迁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年寝蹈,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片登淘。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡箫老,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出黔州,到底是詐尸還是另有隱情耍鬓,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布流妻,位于F島的核電站牲蜀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏绅这。R本人自食惡果不足惜涣达,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望证薇。 院中可真熱鬧度苔,春花似錦、人聲如沸浑度。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箩张。三九已至甩骏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間先慷,已是汗流浹背饮笛。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留熟掂,地道東北人缎浇。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像赴肚,于是被迫代替她去往敵國和親素跺。 傳聞我的和親對象是個殘疾皇子二蓝,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359

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