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

Model的開發(fā)經(jīng)過了文章【一】后茴恰,我們先告一段落巴粪,現(xiàn)在來想想怎么開發(fā)MVC的V和C部分哨鸭。V的部分我們用現(xiàn)成的UITableView民宿,所以接下來重點關注C的部分。

嘗試去開發(fā)Controller類

除了需求【5】之外像鸡,其他的需求都跟Controller相關活鹰,從數(shù)據(jù)的獲取、封裝只估、顯示到控制跳轉(zhuǎn)志群,看起來Controller就會是一個比較多代碼的類了。要在Controller里面測試所以上述功能蛔钙,那么Controller需要暴露很多公共方法和屬性锌云,這樣Controller就比較難看了,而且也不具備好的封裝性吁脱。所以桑涎,我的策略是將一些相對可以獨立的功能單獨封裝成類,然后分別測試它們兼贡,最后測試它們的交互是否正確攻冷,通過這種先肢解在合并的方式來測試和開發(fā)Controller。哪些功能最適合劃分獨立的類來處理呢遍希,很容易想到就是UITableView的數(shù)據(jù)源和代理類等曼,網(wǎng)絡請求類,我們先從這兩個類下手凿蒜,其他的禁谦,后續(xù)有需求在處理。

(一)開發(fā)表格視圖的數(shù)據(jù)源和代理類

這個類應該實現(xiàn)<UITableViewDataSource,UITableViewDelegate>這兩個代理的下面幾個方法:提供行數(shù)和Cell的數(shù)據(jù)源方法废封;提供行高和行被點擊的代理方法枷畏。
繼續(xù)我們之前的做法,沒寫產(chǎn)品代碼之前虱饿,先寫測試用例。創(chuàng)建一個針對這個類的測試類MyTableViewDataSourceTests,添加第一個測試用例測試它是否遵循了UITableViewDataSource協(xié)議氮发。
【tc 2.1.1】

- (void)testConformUITableViewDelegateProtocol{
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    XCTAssertTrue([dataSource conformsToProtocol:@protocol(UITableViewDataSource)]);
}

一開始它沒有編譯通過


image.png

我們創(chuàng)建產(chǎn)品類MyTableViewDataSource渴肉,讓編譯成功,并讓這個測試通過

image.png

遵循了協(xié)議顯然還不夠爽冕,我們需要它確實去實現(xiàn)了我們想要的協(xié)議方法仇祭,新增幾個測試它是否實現(xiàn)了返回行數(shù)和Cell的測試用例。
【tc 2.1.2颈畸,tc 2.1.3】

- (void)test_ImplMethod_tableView_numberOfRowsInSection{
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    XCTAssertTrue([dataSource respondsToSelector:@selector(tableView: numberOfRowsInSection:)]);
}

- (void)test_ImplMethod_tableView_cellForRowAtIndexPath{
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    XCTAssertTrue([dataSource respondsToSelector:@selector(tableView: cellForRowAtIndexPath:)]);
}

顯然一開始它們沒能通過乌奇,因為類只遵循了協(xié)議,未實現(xiàn)協(xié)議方法眯娱,這是一個Red流程礁苗。


我們執(zhí)行Green流程,讓這兩個測試通過徙缴。

#import "MyTableViewDataSource.h"

@implementation MyTableViewDataSource

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return 3;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    return [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"UITableViewCell"];
}

@end
image.png

甭管我們的產(chǎn)品代碼實現(xiàn)是否合理试伙,總之做到能讓我們的測試用例通過,就可以了于样。
現(xiàn)在我們成功執(zhí)行了Green流程疏叨,也發(fā)現(xiàn)了測試代碼里面有可以重構的地方,所以我們執(zhí)行一下Refactor流程穿剖,把測試用例里面用到的重復代碼提取到setUp方法里面蚤蔓。
重構后的測試代碼:

#import <XCTest/XCTest.h>
#import "MyTableViewDataSource.h"

@interface MyTableViewDataSourceTests : XCTestCase

@property (nonatomic, strong) MyTableViewDataSource *dataSource;

@end

@implementation MyTableViewDataSourceTests

- (void)setUp {
    [super setUp];
    self.dataSource = [[MyTableViewDataSource alloc] init];
}

- (void)tearDown {
    self.dataSource = nil;
    [super tearDown];
}

- (void)testConformUITableViewDelegateProtocol{
    XCTAssertTrue([self.dataSource conformsToProtocol:@protocol(UITableViewDataSource)]);
}

- (void)test_ImplMethod_tableView_numberOfRowsInSection{
    XCTAssertTrue([self.dataSource respondsToSelector:@selector(tableView: numberOfRowsInSection:)]);
}

- (void)test_ImplMethod_tableView_cellForRowAtIndexPath{
    XCTAssertTrue([self.dataSource respondsToSelector:@selector(tableView: cellForRowAtIndexPath:)]);
}
@end

重構完成后,我們要確保所有測試用例依然通過糊余。

image.png

對UITableViewDelegate的測試方法一樣秀又,不再贅述。到此啄刹,表格數(shù)據(jù)源代理類的開發(fā)測試先告一段落涮坐。

(二)開發(fā)實現(xiàn)讓數(shù)據(jù)源代理類為表格提供數(shù)據(jù)

這部分代碼是在控制器里面實現(xiàn)的,這部分功能的測試任務是要測試控制器誓军、表格視圖和表格數(shù)據(jù)源代理類這幾個類是否協(xié)作正確袱讹。保證了它們協(xié)作正確,這部分的單元測試任務就算完成了昵时,至于表格看不看得到數(shù)據(jù)捷雕,看到的數(shù)據(jù)是怎樣的,其實這不是單元測試的任務了壹甥,應該由UI測試去覆蓋救巷。
怎么去測試這些類的協(xié)作呢,大概分為幾個部分:
(1)確認控制器有表格視圖句柠、數(shù)據(jù)源代理類的存在浦译。
(2)確認控制器將數(shù)據(jù)源代理類成功賦值給表格作為其數(shù)據(jù)源和代理棒假。
(3)確認表格視圖的行數(shù)、行高和Cell跟其數(shù)據(jù)源代理類提供的數(shù)據(jù)一致精盅。

首先帽哑,寫測試用例去測試(1)
測試先行,創(chuàng)建針對控制器的測試類MyViewControllerTests叹俏,先寫會失敗的測試用例妻枕,執(zhí)行Red流程。
【tc 2.2.1粘驰,測試控制器是否存在一個屬性來引用表格控制器】

#import <XCTest/XCTest.h>
#import <objc/runtime.h>

@interface MyViewControllerTests : XCTestCase

@end

@implementation MyViewControllerTests

- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    [super tearDown];
}
/**
    tc 2.2.1
 */
- (void)test_PropertyExist_TheTableView{
    objc_property_t theTableViewProperty = class_getProperty([MyViewController class], "theTableView");
    XCTAssertTrue(theTableViewProperty != NULL);
}

@end
image.png

再執(zhí)行Green流程屡谐,讓這個測試通過

#import <UIKit/UIKit.h>

@interface MyViewController : UIViewController

@property (nonatomic, strong) NSObject *theTableView;

@end
image.png

很明顯,我們只要給控制器一個名叫theTableView的屬性蝌数,這個測試用例就會通過愕掏,不管屬性的類型是什么。這不是我們想要的結(jié)果籽前,所以我們要追加一個測試用例來規(guī)定這個屬性必須是UITableView類型的亭珍。

【tc 2.2.2,測試屬性theTableView是否是UITableView類型】
/**
 tc 2.2.2
 */
- (void)test_Property_TheTableView_ShouldBeUITableViewType{
    NSString *typeName = [self typeForProperty:@"theTableView" inClass:@"MyViewController"];
    XCTAssertTrue([typeName isEqualToString:@"UITableView"]);
}

/**
 用法:
 1枝哄,如果是block類型的屬性肄梨,這個方法不能識別block的完整sinature,只能告知它是一個block挠锥,名字是什么众羡。
 返回的字符串樣式是:"Block:[屬性名]"。
 2蓖租,如果是id<協(xié)議1粱侣,協(xié)議2>類型,返回字符串樣式是:“<[協(xié)議1]><[協(xié)議2]>”蓖宦。
 3齐婴,如果是普通對象屬性,返回字符串樣式是:“[類名]”稠茂。
 
 @param pName <#pName description#>
 @param cName <#cName description#>
 @return <#return value description#>
 */
+ (NSString *)typeForProperty:(NSString *)pName inClass:(NSString *)cName{
    unsigned int count;
    Class checkClass = NSClassFromString(cName);
    objc_property_t* props = class_copyPropertyList(checkClass, &count);
    for (int i = 0; i < count; i++) {
        objc_property_t property = props[i];
        const char * name = property_getName(property);
        NSString *propertyName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
        if (![propertyName isEqualToString:pName]) {
            continue;
        }
        const char * type = property_getAttributes(property);
        //NSString *attr = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
        NSString * typeString = [NSString stringWithUTF8String:type];
        NSArray * attributes = [typeString componentsSeparatedByString:@","];
        NSString * typeAttribute = [attributes objectAtIndex:0];
        NSString * propertyType = [typeAttribute substringFromIndex:1];
        const char * rawPropertyType = [propertyType UTF8String];
        
        if (strcmp(rawPropertyType, @encode(float)) == 0) {
            //it's a float
        } else if (strcmp(rawPropertyType, @encode(int)) == 0) {
            //it's an int
        } else if (strcmp(rawPropertyType, @encode(id)) == 0) {
            //it's some sort of object
        } else {
            // According to Apples Documentation you can determine the corresponding encoding values
        }
        // 針對block屬性
        if ([attributes containsObject:@"T@?"] &&([attributes containsObject:[NSString stringWithFormat:@"V_%@",propertyName]] || [attributes containsObject:[NSString stringWithFormat:@"V%@",propertyName]])) {
            return [NSString stringWithFormat:@"Block:%@",propertyName];
        }
        
        if ([typeAttribute hasPrefix:@"T@"] && [typeAttribute length] > 1) {
            NSString * typeClassName = [typeAttribute substringWithRange:NSMakeRange(3, [typeAttribute length]-4)];  //turns @"NSDate" into NSDate
            
            Class typeClass = NSClassFromString(typeClassName);
            if (typeClass != nil) {
                // Here is the corresponding class even for nil values
            }
            return typeClassName;
        }
        
    }
    free(props);
    return nil;
}

【tc 2.2.2】使用到了OC的runtime來獲取屬性的類型柠偶,runtime在測試中是很常用的技術,可以說沒有runtime的支持睬关,很多東西都不能或不好去測試诱担。
新加進來的測試用例沒通過,這就是我們想要的結(jié)果电爹,只有當【tc 2.2.1蔫仙,tc 2.2.2】都通過時,屬性的設置才算是正確的丐箩。

image.png

現(xiàn)在摇邦,我們修改產(chǎn)品代碼恤煞,讓兩個測試用例都能通過:

#import <UIKit/UIKit.h>

@interface MyViewController : UIViewController

@property (nonatomic, strong) UITableView *theTableView;

@end
image.png

用同樣的方法,測試寫測試用例涎嚼,然后為控制器添加另一個theDataSource屬性阱州。注意測試屬性theDataSource類型的寫法跟測試屬性theTableView的不太一樣。
【tc 2.2.3法梯,測試是否存在theDataSource屬性】
【tc 2.2.4,測試theDataSource屬性是否是否遵循表格視圖的數(shù)據(jù)源和代理協(xié)議】

/**
 tc 2.2.3
 */
- (void)test_PropertyExist_TheDataSource{
    objc_property_t theTableViewProperty = class_getProperty([MyViewController class], "theDataSource");
    XCTAssertTrue(theTableViewProperty != NULL);
}

/**
 tc 2.2.4
 */
- (void)test_Property_TheDataSource_ShouldConformUITableViewDataSourceAndUITableViewDelegate{
    NSString *typeName = [self typeForProperty:@"theDataSource" inClass:@"MyViewController"];
    XCTAssertTrue([typeName isEqualToString:@"<UITableViewDataSource><UITableViewDelegate>"]);
}

滿足四個測試用例的產(chǎn)品代碼:

#import <UIKit/UIKit.h>

@interface MyViewController : UIViewController

@property (nonatomic, strong) UITableView *theTableView;
@property (nonatomic, strong) id<UITableViewDataSource,UITableViewDelegate> theDataSource;

@end

接下來犀概,我們測試(2)部分的協(xié)作立哑,在控制器存在表格視圖和數(shù)據(jù)源代理類的情況下,數(shù)據(jù)源代理類要作為表格視圖的數(shù)據(jù)源和代理姻灶,當控制器的viewDidLoad方法執(zhí)行之后我們就要確保這一點铛绰。
【tc 2.2.5,測試viewDidLoad之后产喉,控制器是否為表格視圖賦值了數(shù)據(jù)源】
【tc 2.2.6捂掰,測試viewDidLoad之后蚁飒,控制器是否為表格視圖賦值了代理】

/**
 tc 2.2.5
 */
- (void)test_viewDidLoad_ConnetDataSourceToTableView{
    MyViewController *vc = [[MyViewController alloc] init];
    vc.theTableView = [[UITableView alloc] init];
    vc.theDataSource = [[MyTableViewDataSource alloc] init];
    [vc viewDidLoad];
    XCTAssertTrue(vc.theTableView.dataSource == vc.theDataSource);
}
/**
 tc 2.2.6
 */
- (void)test_viewDidLoad_ConnetDelegateToTableView{
    MyViewController *vc = [[MyViewController alloc] init];
    vc.theTableView = [[UITableView alloc] init];
    vc.theDataSource = [[MyTableViewDataSource alloc] init];
    [vc viewDidLoad];
    XCTAssertTrue(vc.theTableView.delegate == vc.theDataSource);
}

實現(xiàn)控制器的viewDidLoad方法袖瞻,讓以上兩個測試用例通過。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.theTableView.dataSource = self.theDataSource;
    self.theTableView.delegate = self.theDataSource;
}

最后瑟蜈,我們測試(3)部分的協(xié)作塞俱,測試表格視圖是否接收到了數(shù)據(jù)源提供的正確數(shù)據(jù)姐帚。

待續(xù)。障涯。罐旗。。
demo:
https://github.com/zard0/TDDListModuleDemo.git

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末唯蝶,一起剝皮案震驚了整個濱河市九秀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌粘我,老刑警劉巖鼓蜒,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異涂滴,居然都是意外死亡友酱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門柔纵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缔杉,“玉大人,你說我怎么就攤上這事搁料』蛳辏” “怎么了系羞?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長霸琴。 經(jīng)常有香客問我椒振,道長,這世上最難降的妖魔是什么梧乘? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任澎迎,我火速辦了婚禮,結(jié)果婚禮上选调,老公的妹妹穿的比我還像新娘夹供。我一直安慰自己,他們只是感情好仁堪,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布哮洽。 她就那樣靜靜地躺著,像睡著了一般弦聂。 火紅的嫁衣襯著肌膚如雪鸟辅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天莺葫,我揣著相機與錄音匪凉,去河邊找鬼。 笑死徙融,一個胖子當著我的面吹牛洒缀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播欺冀,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼树绩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了隐轩?” 一聲冷哼從身側(cè)響起饺饭,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎职车,沒想到半個月后瘫俊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡悴灵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年扛芽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片积瞒。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡川尖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出茫孔,到底是詐尸還是另有隱情叮喳,我是刑警寧澤被芳,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站馍悟,受9級特大地震影響畔濒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锣咒,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一侵状、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧毅整,春花似錦壹将、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妇菱。三九已至承粤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間闯团,已是汗流浹背辛臊。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留房交,地道東北人彻舰。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像候味,于是被迫代替她去往敵國和親刃唤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

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