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

第【三】篇主要展示了如何測試驅(qū)動地開發(fā)表格試圖的數(shù)據(jù)源類,保證其為表格提供正確的行數(shù)和Cells芹啥。這一篇主要將繼續(xù)展示如何測試驅(qū)動地開發(fā)表格試圖的數(shù)據(jù)源兼代理類,要實(shí)現(xiàn):【功能4-1】當(dāng)數(shù)據(jù)源更新數(shù)據(jù)時(shí)厢洞,刷新表格祠挫;【功能4-2】當(dāng)表格的Cell被點(diǎn)擊時(shí),代理類響應(yīng)點(diǎn)擊事件蜈膨,并正確地傳遞參數(shù)給控制器屿笼;【功能4-3】控制器在接收到數(shù)據(jù)源兼代理傳遞的參數(shù)后牺荠,可以根據(jù)參數(shù)跳轉(zhuǎn)到正確的下一級界面。

一驴一,開發(fā)【功能4-1】

這一功能的實(shí)現(xiàn)涉及三個(gè)環(huán)節(jié):
【環(huán)節(jié)4-1-1】數(shù)據(jù)源類的數(shù)據(jù)被更新休雌。
【環(huán)節(jié)4-1-2】數(shù)據(jù)源類通知控制器數(shù)據(jù)被更新了。
【環(huán)節(jié)4-1-3】控制器調(diào)用表格的刷新方法刷新表格肝断。

開發(fā)【環(huán)節(jié)4-1-1】

這一環(huán)節(jié)當(dāng)前不需要做什么測試和開發(fā)杈曲,數(shù)據(jù)源類已經(jīng)有了可以接納數(shù)據(jù)的theDataArray屬性,這就可以了胸懈。

開發(fā)【環(huán)節(jié)4-1-2】

【Refactor: 提取之前測試用到的判斷屬性類型的方法担扑,使之成為一個(gè)公用的類方法】
這些跟測試相關(guān)的輔助類都只是放在測試的target里面而已,不出現(xiàn)在產(chǎn)品代碼target趣钱。

#import <Foundation/Foundation.h>

@interface NSObject (TestingHelper)

/**
 
 用來判斷一個(gè)類的某個(gè)屬性的類型是什么
 
 用法:
 1涌献,如果是block類型的屬性,這個(gè)方法不能識別block的完整sinature首有,只能告知它是一個(gè)block燕垃,名字是什么。
 返回的字符串樣式是:"Block:[屬性名]"绞灼。
 2利术,如果是id<協(xié)議1,協(xié)議2>類型低矮,返回字符串樣式是:“<[協(xié)議1]><[協(xié)議2]>”印叁。
 3,如果是普通對象屬性军掂,返回字符串樣式是:“[類名]”轮蜕。
 
 @param pName 屬性名稱
 @param cName 類名稱
 @return 屬性類型標(biāo)識字符串
 */
+ (NSString *)typeForProperty:(NSString *)pName inClass:(NSString *)cName;

@end

【Red:tc 4.1,測試數(shù)據(jù)源類有一個(gè)更新block的屬性】

//  MyTableViewDataSourceTests.m

#import <XCTest/XCTest.h>
#import "MyTableViewDataSource.h"
#import "NSObject+TestingHelper.h"

/**
 tc 4.1
 */
- (void)test_HasAnUpdateBlock{
    NSString *type = [NSObject typeForProperty:@"updateBlock" inClass:@"MyTableViewDataSource"];
    XCTAssertTrue([type isEqualToString:@"Block:updateBlock"]);
}

【Green:數(shù)據(jù)源類頭文件添加updateBlock屬性讓測試tc 4.1通過】

#import <UIKit/UIKit.h>

@interface MyTableViewDataSource : NSObject<UITableViewDataSource,UITableViewDelegate>

@property (nonatomic, strong) NSArray *theDataArray;
@property (nonatomic, copy) void(^updateBlock)();

@end

【Red:tc 4.2蝗锥,測試據(jù)源類數(shù)據(jù)更新時(shí)updateBlock如果存在會被調(diào)用】

//  MyTableViewDataSourceTests.m

/**
 tc 4.2
 */
- (void)test_ExecuteUpdateBlockIfExistWhenTheDataArrayUpdated{
    __block BOOL update = NO;
    self.dataSource.updateBlock = ^{
        update = YES;
    };
    self.dataSource.theDataArray = @[];
    XCTAssertTrue(update);
}

【Green:數(shù)據(jù)源類.m文件修改setTheDataArray:方法讓測試tc 4.2通過】

//  MyTableViewDataSource

- (void)setTheDataArray:(NSArray *)theDataArray{
    _theDataArray = theDataArray;
    if (self.updateBlock) {
        self.updateBlock();
    }
}

開發(fā)【環(huán)節(jié)4-1-3】

【tc 4.3跃洛,測試控制器的數(shù)據(jù)源有更新后表格會刷新】
這個(gè)測試用例的被測對象是控制器,測試動作是改變它的theDataSource屬性的狀態(tài)终议,測試要驗(yàn)證的是它的theTableView屬性狀態(tài)有沒有對測試動作做出相應(yīng)狀態(tài)改變汇竭。這里的難點(diǎn)是如何驗(yàn)證theTableView改變了狀態(tài),畢竟測試動作不會造成它任何公共屬性的值的變更穴张。不過细燎,狀態(tài)的變更更廣泛一點(diǎn)來講不一定是屬性值的變更,也可以表現(xiàn)為它調(diào)用了自身的某些方法皂甘。所以這里的測試驗(yàn)證轉(zhuǎn)變成如何驗(yàn)證theTableView調(diào)用了reloadData方法玻驻。驗(yàn)證UITableView類對象是否調(diào)用了reloadData的方法我并不知道,感覺也不好做驗(yàn)證偿枕。好在有一種我們在單元測試中經(jīng)常會使用的技術(shù)可以解決這個(gè)問題璧瞬,那就是“假對象替換”户辫,這種技術(shù)有很多應(yīng)用場景,這里的應(yīng)用場景可以說明為:創(chuàng)建一個(gè)與真實(shí)使用的對象有相同外觀行為的假對象嗤锉,這樣假對象就可以被調(diào)用方當(dāng)真對象使用渔欢,由于假對象是我們自己定義的,我們可以讓假對象具備足夠的可測試性档冬,來方便我們驗(yàn)證它是如何被調(diào)用方使用的膘茎,驗(yàn)證了測試情形中的假對象如何被使用也就驗(yàn)證了真實(shí)情形中的真對象如何被使用。我的解釋可能不夠好酷誓,我希望通過對這個(gè)測試用例的演示來更好地說明披坏。
我們這里需要用假對象替換的對象是控制器的theTableView,為了讓假對象跟它有一樣的行為和外觀盐数,我們讓假對象的類繼承于它的類UITableView棒拂,這個(gè)假對象只在測試中才使用,所以玫氢,我們只把它添加到測試target:

@interface FakeTableView : UITableView

/// 可以在要被觀察的方法里面使用這個(gè)block帚屉,讓外界知道方法是否被調(diào)用,調(diào)用時(shí)傳參是什么
@property (nonatomic, copy) void(^callMethodBlock)(NSString *methodName, NSDictionary *parameters);

@end


#import "FakeTableView.h"

@implementation FakeTableView

/**
 重寫系統(tǒng)方法漾峡,讓它支持調(diào)用時(shí)可以被觀察
 */
- (void)reloadData{
    if (self.callMethodBlock) {
        self.callMethodBlock(@"reloadData", nil);
    }
}
@end

當(dāng)FakeTableView的對象攻旦,替換了theTableView的真實(shí)對象被控制器調(diào)用其執(zhí)行reloadData方法時(shí),這個(gè)執(zhí)行就會被觀察到生逸。
【Red:tc 4.3牢屋,測試表格數(shù)據(jù)源更新時(shí)表格應(yīng)該調(diào)用reloadData方法刷新數(shù)據(jù)】

// MyViewControllerTests.m

#import "FakeTableView.h"

/**
 tc 4.3
 */
- (void)test_reloadTableViewWhenDataSouceGetNewData{
    MyViewController *vc = [[MyViewController alloc] init];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    FakeTableView *tableView = [[FakeTableView alloc] init];
    __block NSString *calledMethod;
    tableView.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
        calledMethod = methodName;
    };
    vc.theTableView = tableView;
    vc.theDataSource = dataSource;
    [vc viewDidLoad];
    dataSource.theDataArray = @[];
    XCTAssertTrue([calledMethod isEqualToString:@"reloadData"]);
}

【Green:為控制器新添刷新表格邏輯讓測試通過】
這里發(fā)現(xiàn)控制器的theDataSource屬性并不支持真實(shí)數(shù)據(jù)源新添屬性的使用,因?yàn)閕d<UITableViewDataSource,UITableViewDelegate>類型的對象并不存在那樣的屬性槽袄。我不準(zhǔn)備把theDataSource的類型換成專門數(shù)據(jù)源及代理類的對象的類型烙无,我希望這個(gè)控制器以后可以使用不同的數(shù)據(jù)類源做邏輯相同的操作,所以遍尺,我新添了一個(gè)我們自定義數(shù)據(jù)源協(xié)議讓這個(gè)屬性遵循截酷,我預(yù)感這個(gè)協(xié)議既支持了我想要的數(shù)據(jù)源和代理類應(yīng)有的拓展,而這些拓展也具備一定的通用性乾戏,可以被其他數(shù)據(jù)源使用迂苛。
在產(chǎn)品target添加自定義數(shù)據(jù)源協(xié)議:

#import <Foundation/Foundation.h>

@protocol MyDataSourceProtocol <NSObject>

@optional
@property (nonatomic, copy) void(^updateBlock)();

@end

讓數(shù)據(jù)源類遵守和實(shí)現(xiàn)這個(gè)協(xié)議

#import "MyDataSourceProtocol.h"

@interface MyTableViewDataSource : NSObject<UITableViewDataSource,UITableViewDelegate,MyDataSourceProtocol>

@property (nonatomic, strong) NSArray *theDataArray;

@end

//  MyTableViewDataSource.m

@implementation MyTableViewDataSource
@synthesize updateBlock;
//  其他代碼。鼓择。灾部。
@end

修改控制器屬性theDataSource的類型:

#import "MyDataSourceProtocol.h"

@interface MyViewController : UIViewController

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

@end

在控制器viewDidLoad里面實(shí)現(xiàn)刷新邏輯:

//  MyViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) wSelf = self;
    self.theDataSource.updateBlock = ^{
        __strong typeof(self) sSelf = wSelf;
        [sSelf.theTableView reloadData];
    };
    self.theTableView.dataSource = self.theDataSource;
    self.theTableView.delegate = self.theDataSource;
}

重新運(yùn)行所有控制器的測試,發(fā)現(xiàn)【tc 4.3】通過了惯退,不過一個(gè)之前的用例【tc 2.2.4】失敗了,它的失敗不是我們產(chǎn)品代碼出了bug从藤,而是這個(gè)用例已經(jīng)過時(shí)需要更新催跪,修改它讓它在新代碼下繼續(xù)通過锁蠕。

//  MyViewControllerTests.m

/**
 tc 2.2.4
 */
- (void)test_Property_TheDataSource_ShouldConformUITableViewDataSourceAndUITableViewDelegate{
    NSString *typeName = [NSObject typeForProperty:@"theDataSource" inClass:@"MyViewController"];
    // 舊的驗(yàn)證
    //XCTAssertTrue([typeName isEqualToString:@"<UITableViewDataSource><UITableViewDelegate>"]);
    // 新的驗(yàn)證
    XCTAssertTrue([typeName isEqualToString:@"<UITableViewDataSource><UITableViewDelegate><MyDataSourceProtocol>"]);
}

二,開發(fā)【功能4-2】

【Red:tc 4.4懊蒸,測試代理類有一個(gè)專門的cellTapBlock】

//  MyTableViewDataSourceTests

/**
 tc 4.4
 */
- (void)test_HasACellTapBlock{
    NSString *type = [NSObject typeForProperty:@"cellTapBlock" inClass:@"MyTableViewDataSource"];
    XCTAssertTrue([type isEqualToString:@"Block:cellTapBlock"]);
}

【Green:往MyDataSourceProtocol添加cellTapBlock屬性荣倾,并讓代理類實(shí)現(xiàn)它】
只要是block就能讓測試通過,我們先不管這個(gè)block要怎么設(shè)計(jì)骑丸,先把一個(gè)block加進(jìn)來舌仍。

@protocol MyDataSourceProtocol <NSObject>

@optional
@property (nonatomic, copy) void(^updateBlock)();
@property (nonatomic, copy) void(^cellTapBlock)();

@end

//  MyTableViewDataSource.m

@implementation MyTableViewDataSource
@synthesize cellTapBlock;
//  其他代碼。通危。铸豁。
@end

【Red:tc 4.5,測試cell的點(diǎn)擊事件代理方法執(zhí)行后cellTapBlock也會被執(zhí)行】

//  MyTableViewDataSourceTests

/**
 tc 4.5
 */
- (void)test_ExecuteCellTapBlockIfCellSelectedMethodCalled{
    __block BOOL called = NO;
    self.dataSource.cellTapBlock = ^{
        called = YES;
    };
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    [self.dataSource tableView:self.theTableView didSelectRowAtIndexPath:indexPath];
    XCTAssertTrue(called);
}

【Green:修改代理類的cell點(diǎn)擊代理方法邏輯菊碟,讓上面測試通過】

//  MyTableViewDataSource.m

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    if (self.cellTapBlock) {
        self.cellTapBlock();
    }
}

【Red:tc 4.6节芥,測試cell的點(diǎn)擊事件代理方法執(zhí)行后cellTapBlock能拿到cell對應(yīng)的行號】

//  MyTableViewDataSourceTests

/**
 tc 4.6
 */
- (void)test_CellTapBlockReceiveDataOfTappedCell{
    __block NSInteger row = 0;
    self.dataSource.cellTapBlock = ^(NSIndexPath *indexPath){
        row = indexPath.row;
    };
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:1 inSection:0];
    [self.dataSource tableView:self.theTableView didSelectRowAtIndexPath:indexPath];
    XCTAssertTrue(row == 1);
}

這時(shí)候我們終于知道cellTapBlock要設(shè)計(jì)成什么樣了,所以我們修改之前的cellTapBlock逆害,為它添加一個(gè)NSIndexPath參數(shù)头镊,同時(shí)修改【tc 4.5】讓它適應(yīng)測試有參數(shù)的block。
【Green:修改代理類cell被選擇的代理方法實(shí)現(xiàn)邏輯讓測試通過】

// MyTableViewDataSource

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    if (self.cellTapBlock) {
        self.cellTapBlock(indexPath);
    }
}

三魄幕,開發(fā)【功能4-3】

這個(gè)功能要求控制器從表格代理類拿到被點(diǎn)擊cell的行號后相艇,根據(jù)行號查找數(shù)據(jù)源對應(yīng)的數(shù)據(jù),解析數(shù)據(jù)根據(jù)數(shù)據(jù)的type選擇push對應(yīng)類型的控制器纯陨,同時(shí)把數(shù)據(jù)的id傳給被push的控制器坛芽。從代理類執(zhí)行了cell點(diǎn)擊代理方法后的所有過程都是在控制器里面完成的,控制器沒有任何暴露的公共屬性來記錄這些過程的狀態(tài)變化队丝,除了它的navigationController這個(gè)屬性靡馁,push執(zhí)行之后,它的topViewController將變成新的控制器机久,我們用這個(gè)屬性來檢測控制器是否推出了正確的下級界面臭墨。
【Red:tc 4.7,測試點(diǎn)擊A類型數(shù)據(jù)cell時(shí)push到ATypeViewController】

// MyViewControllerTests.m

/**
 tc 4.7 
 */
- (void)test_pushATypeViewControllerIfTapATypeCell{
    MyViewController *myVC = [[MyViewController alloc] init];
    UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:myVC];
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    UITableView *tableView = [[UITableView alloc] init];
    myVC.theTableView = tableView;
    myVC.theDataSource = dataSource;
    [myVC viewDidLoad];
    myVC.theDataSource.theDataArray = @[@{@"type":@0,@"title":@"Type A Title",@"someId":@"0001"},@{@"type":@1,@"title":@"Type B Title",@"someId":@"0002"}];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    [myVC.theDataSource tableView:myVC.theTableView didSelectRowAtIndexPath:indexPath];
    XCTAssertTrue([myVC.navigationController.topViewController isKindOfClass:[ATypeViewController class]]);
}

【Green:修改產(chǎn)品代碼膘盖,讓測試通過】
修改MyDataSourceProtocol胧弛,添加theDataArray屬性,是的數(shù)據(jù)源的數(shù)據(jù)字典數(shù)組可以在控制器內(nèi)部被通過self.theDataSource.theDataArray這種方式使用:

//  MyDataSourceProtocol.h
// 添加
@property (nonatomic, strong) NSArray *theDataArray;

//  MyTableViewDataSource.m
// 添加
@synthesize theDataArray = _theDataArray;

創(chuàng)建要被push的新的視圖控制器:

@interface ATypeViewController : UIViewController

@property (nonatomic, copy) NSString *someId;

@end

修改控制器viewDidLoad的邏輯:

//  MyViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) wSelf = self;
    self.theDataSource.updateBlock = ^{
        __strong typeof(self) sSelf = wSelf;
        [sSelf.theTableView reloadData];
    };
    self.theDataSource.cellTapBlock = ^(NSIndexPath *indexPath) {
        __strong typeof(self) sSelf = wSelf;
        NSDictionary *data = sSelf.theDataSource.theDataArray[indexPath.row];
        if ([data[@"type"] integerValue] == 0) {
            ATypeViewController *vc = [[ATypeViewController alloc] init];
            [sSelf.navigationController pushViewController:vc animated:NO];
        }
    };
    self.theTableView.dataSource = self.theDataSource;
    self.theTableView.delegate = self.theDataSource;
}

經(jīng)過這些修改后侠畔,【tc 4.7】可以運(yùn)行通過结缚。但是這種測試方式有個(gè)缺點(diǎn),那就是產(chǎn)品代碼里面的[self.navigationController pushViewController:vc animated:NO];方法的animated:參數(shù)必須傳NO,否則在測試環(huán)境中就不能push成功软棺,會發(fā)現(xiàn)红竭,雖然產(chǎn)品代碼執(zhí)行了push方法,但是導(dǎo)航控制器的topViewController指向的仍然是MyViewController對象而非ATypeViewController對象,也就是push實(shí)際沒成功茵宪。這個(gè)缺點(diǎn)不能容忍最冰,因?yàn)橐话阄覀僷ush都會把動畫參數(shù)設(shè)為YES的。
我決定換一種思路去測試控制器的push稀火。根據(jù)第【三】篇提到的不要用單元測試去測系統(tǒng)框架的類的測試原則暖哨,我們其實(shí)不需要去測試最終push的結(jié)果,只需要去確認(rèn)導(dǎo)航控制器調(diào)用了push方法凰狞,而且調(diào)用push方法時(shí)傳遞的參數(shù)是正確的篇裁,這就可以了。為了實(shí)現(xiàn)這種測試思路赡若,我們要像前面創(chuàng)建FakeTableView一樣达布,創(chuàng)建一個(gè)FakeNavigationController,用它來替換真實(shí)場景中的導(dǎo)航控制器斩熊,然后我們觀察它的push方法有沒有被執(zhí)行往枣,并拿到被pushed的控制器,驗(yàn)證是不是我們想要的控制器粉渠。
我發(fā)現(xiàn)前面給FakeTableView使用的用來檢測方法是否被調(diào)用的block也適合這個(gè)FakeNavigationViewController分冈,所以我要把這部分代碼抽取到NSObject+TestingHelper這個(gè)類別,方便以后的應(yīng)用需求霸株。
【Refactor:提取測試輔助共用代碼雕沉,提高測試代碼重新性】
補(bǔ)充TestingHelper類別的功能。
NSObject + TestingHelper.h文件:

@interface NSObject (TestingHelper)

/// 可以在要被觀察的方法里面使用這個(gè)block去件,讓外界知道方法是否被調(diào)用坡椒,調(diào)用時(shí)傳參是什么
@property (nonatomic, copy) void(^callMethodBlock)(NSString *methodName, NSDictionary *parameters);

/**
 方便調(diào)用callMethodBlock的方法

 @param methodName <#methodName description#>
 @param parasDic <#parasDic description#>
 */
- (void)callMethod:(NSString *)methodName parameters:(NSDictionary *)parasDic;

// 其他代碼。尤溜。倔叼。。

@end

NSObject + TestingHelper.m文件:

#import "NSObject+TestingHelper.h"
#import <objc/runtime.h>

static const void * CallMethodBlockKey = &CallMethodBlockKey;

@implementation NSObject (TestingHelper)

- (void(^)(NSString *methodName, NSDictionary *parameters))callMethodBlock{
    return objc_getAssociatedObject(self, CallMethodBlockKey);
}

- (void)setCallMethodBlock:(void (^)(NSString *, NSDictionary *))callMethodBlock{
    objc_setAssociatedObject(self, CallMethodBlockKey, callMethodBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

/**
 方便調(diào)用callMethodBlock的方法
 
 @param methodName <#methodName description#>
 @param parasDic <#parasDic description#>
 */
- (void)callMethod:(NSString *)methodName parameters:(NSDictionary *)parasDic{
    if (self.callMethodBlock) {
        self.callMethodBlock(methodName, parasDic);
    }
}

//  其他代碼宫莱。丈攒。。

@end

實(shí)現(xiàn)FakeNavigationViewController類授霸。
FakeNavigationViewController.h文件:

#import <UIKit/UIKit.h>
#import "NSObject+TestingHelper.h"


@interface FakeNavigationViewController : UINavigationController

/**
 拿到push方法名稱

 @return <#return value description#>
 */
+ (NSString *)pushMethodName;

/**
 拿到push方法參數(shù)字典的控制器參數(shù)的key

 @return <#return value description#>
 */
+ (NSString *)pushControllerParaKey;

/**
 拿到push方法參數(shù)字典的是否執(zhí)行動畫參數(shù)的key

 @return <#return value description#>
 */
+ (NSString *)pushAnimateParaKey;

@end


FakeNavigationViewController.m文件:

#import "FakeNavigationViewController.h"


@interface FakeNavigationViewController ()

@end

@implementation FakeNavigationViewController

/**
 重寫系統(tǒng)方法巡验,讓這個(gè)方法的調(diào)用可以被驗(yàn)證

 @param viewController <#viewController description#>
 @param animated <#animated description#>
 */
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
    // 注意調(diào)用一下super方法,否則在執(zhí)行initWithRootViewController:方法時(shí)就不能達(dá)到預(yù)期效果了碘耳。
    [super pushViewController:viewController animated:animated];
    
    [self callMethod:[FakeNavigationViewController pushMethodName]
          parameters:@{
                       [FakeNavigationViewController pushControllerParaKey]:viewController,
                       [FakeNavigationViewController pushAnimateParaKey]:@(animated)}];
}

+ (NSString *)pushMethodName{
    return @"pushViewController:animated:";
}

+ (NSString *)pushControllerParaKey{
    return @"Controller";
}

+ (NSString *)pushAnimateParaKey{
    return @"Animate";
}

@end

【Red:tc 4.8 測試點(diǎn)擊A類型數(shù)據(jù)cell時(shí)显设,導(dǎo)航控制器會調(diào)用push方法,push的控制器是ATypeViewController對象辛辨,push的animate參數(shù)是YES】

//  MyViewControllerTests.m

/**
 tc 4.8
 */
- (void)test_pushMethodIsCalledWithATypeViewControllerIfTapATypeCell{
    MyViewController *myVC = [[MyViewController alloc] init];
    FakeNavigationViewController *nav = [[FakeNavigationViewController alloc] initWithRootViewController:myVC];
    __block NSString *name;
    __block NSDictionary *paras;
    nav.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
        name = methodName;
        paras = parameters;
    };
    MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
    UITableView *tableView = [[UITableView alloc] init];
    myVC.theTableView = tableView;
    myVC.theDataSource = dataSource;
    [myVC viewDidLoad];
    myVC.theDataSource.theDataArray = @[@{@"type":@0,@"title":@"Type A Title",@"someId":@"0001"},@{@"type":@1,@"title":@"Type B Title",@"someId":@"0002"}];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    [myVC.theDataSource tableView:myVC.theTableView didSelectRowAtIndexPath:indexPath];
    XCTAssertTrue([name isEqualToString:[FakeNavigationViewController pushMethodName]]);
    XCTAssertTrue([paras[[FakeNavigationViewController pushControllerParaKey]] isKindOfClass:[ATypeViewController class]]);
    XCTAssertEqual([paras[[FakeNavigationViewController pushAnimateParaKey]] boolValue] , YES);
}

【Green:修改控制器viewDidLoad邏輯捕捂,把push方法的animate參數(shù)設(shè)為YES】
修改代碼后评姨,重新執(zhí)行所有測試引润,【tc 4.8】可以通過埃元,【tc 4.7】如預(yù)料的失敗了自脯,因?yàn)樗恢С钟袆赢嫷膒ush棕诵,現(xiàn)在有了【tc 4.8】肚邢,我們可以把【tc 4.7】刪掉了谬运。
在進(jìn)一步去完善cell的跳轉(zhuǎn)測試之前离福,可以先執(zhí)行一下Refactor流程溅呢,因?yàn)楝F(xiàn)在MyViewControllerTests.m里面有不少冗余測試代碼澡屡,而且如果不執(zhí)行重構(gòu)的話即將新增的測試用例會帶來更多的冗余代碼,重構(gòu)測試代碼后咐旧,將使得接下來要寫的每個(gè)測試用例代碼行數(shù)更少驶鹉,所以現(xiàn)在這個(gè)點(diǎn)重構(gòu)很有好處。而產(chǎn)品代碼里面铣墨,MyViewController里面執(zhí)行跳轉(zhuǎn)時(shí)解析的數(shù)據(jù)源還是字典室埋,并且無法使用我在MyModel里面定義好的類型枚舉變量,我希望修改為讓它用的是MyModel對象伊约,并且使用上類型枚舉姚淆。
【Refactor:減少測試類里面的冗余代碼】

@interface MyViewControllerTests : XCTestCase

@property (nonatomic, strong) UITableView *theTableView;
@property (nonatomic, strong) MyTableViewDataSource *theDataSource;
@property (nonatomic, strong) MyViewController *theController;

@end

@implementation MyViewControllerTests

- (void)setUp {
    [super setUp];
    self.theTableView = [[UITableView alloc] init];
    self.theDataSource = [[MyTableViewDataSource alloc] init];
    self.theController = [[MyViewController alloc] init];
}

- (void)tearDown {
    self.theDataSource = nil;
    self.theTableView = nil;
    self.theController = nil;
    [super tearDown];
}

#pragma mark - 重用方法

/**
 這部分重用代碼我不寫在setUp方法,而要寫成這個(gè)公共方法來讓測試用例調(diào)用屡律,因?yàn)椋? 1腌逢,這部分代碼只適合一部分測試用例使用,在用不上setUp里面這些代碼的測試用例要減少它們執(zhí)行無謂的代碼的時(shí)間超埋。
 2搏讶,這部分代碼也是測試用例比較重要的前置步驟,如果寫在setUp霍殴,以后查看相關(guān)測試用例代碼時(shí)媒惕,可能不自覺會忽略這些前置條件,降低了代碼的說明性来庭。
 */
- (void)setupDataSourceAndTableViewThenDoViewDidLoad{
    self.theController.theTableView = self.theTableView;
    self.theController.theDataSource = self.theDataSource;
    [self.theController viewDidLoad];
}

#pragma mark - 測試用例

重構(gòu)測試代碼有些地方要注意妒蔚,比如上面代碼里面的注釋提到的點(diǎn)。
現(xiàn)在這個(gè)文件里面的測試用例代碼變得更簡潔了巾腕,運(yùn)行一次面睛,全部用例繼續(xù)通過,說明重構(gòu)沒問題尊搬。
【Refactor:重構(gòu)MyViewController類的產(chǎn)品代碼】
我發(fā)現(xiàn)要重構(gòu)跳轉(zhuǎn)邏輯代碼來達(dá)到我上面所提的目的叁鉴,并不是一個(gè)微調(diào)整就可以的。它讓我意識到當(dāng)前產(chǎn)品代碼架構(gòu)有問題佛寿,我明明希望MyViewController的theDataSource是一個(gè)能夠替換不同實(shí)現(xiàn)的數(shù)據(jù)源引用幌墓,來達(dá)到讓MyViewController可以重用的目的但壮;同時(shí),我卻不斷要求往這個(gè)這個(gè)引用指向的類型上去添加只屬于當(dāng)前模塊才有的功能常侣,如果說目前追加的MyDataSourceProtocol上面的內(nèi)容還屬于有一定通用性的話蜡饵,那么我剛剛準(zhǔn)備的讓數(shù)據(jù)源公共屬性cellTapBlock的傳參變成傳MyModel對象,好讓控制器在這個(gè)block里面不用解析字典數(shù)據(jù)胳施,直接讀取model數(shù)據(jù)溯祸,直接使用ModelType枚舉,這一意圖則將徹底讓theDataSource屬性淪落到只接受當(dāng)前模塊數(shù)據(jù)源的境地了∥杷粒現(xiàn)在焦辅,為了實(shí)現(xiàn)上述重構(gòu)需求,我有兩個(gè)代碼架構(gòu)的方向可考慮:第一椿胯,丟棄MyViewController的重用想法筷登,讓它只與當(dāng)前模塊的數(shù)據(jù)源結(jié)合,成為專用控制器哩盲;第二前方,依然堅(jiān)持要讓MyViewController通過更換數(shù)據(jù)源實(shí)現(xiàn)重用,只是廉油,不能單單只有數(shù)據(jù)源可替換惠险,還得把Cell的跳轉(zhuǎn)邏輯也抽離出來,讓其類似于數(shù)據(jù)源一樣被MyViewController所依賴娱两,然后也可以替換莺匠,來達(dá)到讓MyViewController徹底不去依賴當(dāng)前模塊任何一項(xiàng)專有功能的實(shí)現(xiàn)細(xì)節(jié),轉(zhuǎn)而只依賴它們遵循的協(xié)議十兢。

看上去我更喜歡第二種方案趣竣,畢竟易修改、易拓展旱物、可重用的架構(gòu)怎么也比無法重用遥缕、耦合性強(qiáng)的架構(gòu)好吧∠海可是单匣,學(xué)習(xí)測試驅(qū)動開發(fā)的同時(shí)也讓我看重了一項(xiàng)軟件開發(fā)原則,即不要去做多余的事情宝穗,也許這個(gè)原則這樣表達(dá)還不夠貼切户秤。那么參考老外們的說法,英文里這一原則叫YAGNT(You Aren't Going to Need It)逮矛。只要我們的產(chǎn)品代碼鸡号,讓我們的所有的測試都通過了,那么须鼎,我們的開發(fā)工作就做完了鲸伴。那些我們費(fèi)盡心思設(shè)計(jì)的精巧架構(gòu)府蔗,或許并不需要。

糾結(jié)啊汞窗,到底要怎么選擇姓赤?

待續(xù)。仲吏。不铆。。
demo:
https://github.com/zard0/TDDListModuleDemo.git

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末裹唆,一起剝皮案震驚了整個(gè)濱河市狂男,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌品腹,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,222評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件红碑,死亡現(xiàn)場離奇詭異舞吭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)析珊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評論 3 385
  • 文/潘曉璐 我一進(jìn)店門羡鸥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人忠寻,你說我怎么就攤上這事惧浴。” “怎么了奕剃?”我有些...
    開封第一講書人閱讀 157,720評論 0 348
  • 文/不壞的土叔 我叫張陵衷旅,是天一觀的道長。 經(jīng)常有香客問我纵朋,道長柿顶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,568評論 1 284
  • 正文 為了忘掉前任操软,我火速辦了婚禮嘁锯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘聂薪。我一直安慰自己家乘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,696評論 6 386
  • 文/花漫 我一把揭開白布藏澳。 她就那樣靜靜地躺著仁锯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪笆载。 梳的紋絲不亂的頭發(fā)上扑馁,一...
    開封第一講書人閱讀 49,879評論 1 290
  • 那天涯呻,我揣著相機(jī)與錄音,去河邊找鬼腻要。 笑死复罐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的雄家。 我是一名探鬼主播效诅,決...
    沈念sama閱讀 39,028評論 3 409
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼趟济!你這毒婦竟也來了乱投?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,773評論 0 268
  • 序言:老撾萬榮一對情侶失蹤顷编,失蹤者是張志新(化名)和其女友劉穎戚炫,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體媳纬,經(jīng)...
    沈念sama閱讀 44,220評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡双肤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,550評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钮惠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茅糜。...
    茶點(diǎn)故事閱讀 38,697評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖素挽,靈堂內(nèi)的尸體忽然破棺而出蔑赘,到底是詐尸還是另有隱情,我是刑警寧澤预明,帶...
    沈念sama閱讀 34,360評論 4 332
  • 正文 年R本政府宣布缩赛,位于F島的核電站,受9級特大地震影響贮庞,放射性物質(zhì)發(fā)生泄漏峦筒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,002評論 3 315
  • 文/蒙蒙 一窗慎、第九天 我趴在偏房一處隱蔽的房頂上張望物喷。 院中可真熱鬧,春花似錦遮斥、人聲如沸峦失。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,782評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尉辑。三九已至,卻和暖如春较屿,著一層夾襖步出監(jiān)牢的瞬間隧魄,已是汗流浹背卓练。 一陣腳步聲響...
    開封第一講書人閱讀 32,010評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留购啄,地道東北人襟企。 一個(gè)月前我還...
    沈念sama閱讀 46,433評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像狮含,于是被迫代替她去往敵國和親顽悼。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,587評論 2 350

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