第【三】篇主要展示了如何測試驅(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