第【四】篇的最后且改,我說道我碰到了一個令人糾結(jié)的代碼重構(gòu)的選擇方案問題,到底選擇讓控制器成為可重用的控制器還是成為專用的控制器厅瞎。讓控制器可重用的重構(gòu)方案迎捺,會讓代碼具備更好的重用性慌闭、可變性和可測試性别威,我喜歡這種追求躯舔,我估摸著要做到這一點,工作量不會太大省古,所以我選擇這種重構(gòu)方案粥庄。
那么現(xiàn)在最主要的是重構(gòu)cell的跳轉(zhuǎn)部分的代碼,我將把這部分代碼從控制器里面剝離出來衫樊,放到獨立的跳轉(zhuǎn)類里面飒赃,然后讓控制器通過協(xié)議依賴這個跳轉(zhuǎn)類。我會讓數(shù)據(jù)源代理類通過控制器跟跳轉(zhuǎn)類協(xié)作科侈,通過數(shù)據(jù)源代理類傳遞的數(shù)據(jù)決定讓跳轉(zhuǎn)類怎么執(zhí)行跳轉(zhuǎn)载佳。跳轉(zhuǎn)類引入了專有模塊的數(shù)據(jù)model,要跳轉(zhuǎn)的控制器臀栈,這兩個類都不應(yīng)該要求控制器知道蔫慧,所以跳轉(zhuǎn)類與數(shù)據(jù)源代理類相互間協(xié)作所使用到的公共方法都不涉及具體數(shù)據(jù)模型,數(shù)據(jù)源代理類像外界傳的是id類型的數(shù)據(jù)权薯,跳轉(zhuǎn)類拿到id類型的數(shù)據(jù)后姑躲,自身判斷它是不是自己所需的model,是的話就解析model做跳轉(zhuǎn)盟蚣,否則什么也不做黍析。雖然如果讓跳轉(zhuǎn)類直接被數(shù)據(jù)源代理類所依賴的話,那么它們之間的交互可以使用模塊專有的數(shù)據(jù)model屎开,似乎很多事情會更方便阐枣,但是因為跳轉(zhuǎn)類跟UI打交道,他需要知道要跳轉(zhuǎn)的目的控制器和執(zhí)行跳轉(zhuǎn)的導(dǎo)航控制器奄抽,而數(shù)據(jù)源代理類蔼两,為了方便測試,并保持它的純粹性逞度,我希望它只做數(shù)據(jù)邏輯额划,不依賴UI相關(guān)的類(除了UITableViewCell),所以跳轉(zhuǎn)類不會跟數(shù)據(jù)源類有直接關(guān)系档泽,它將被控制器強引用俊戳,被控制器在數(shù)據(jù)源代理類響應(yīng)表格cell點擊事件后所使用。它被使用的公共方法將在它與控制器約定的協(xié)議里面定義馆匿。
有了想法后抑胎,我們繼續(xù)用測試驅(qū)動的方式進行開發(fā)。
一甜熔,為控制器添加一個引用跳轉(zhuǎn)類的屬性。
【Red:tc 5.1突倍,控制器屬性theJumper遵循JumperProtocol協(xié)議】
MyViewControllerTests.m文件:
/**
tc 5.1
*/
- (void)test_Property_TheJumper_ConformJumperProtocol{
NSString *typeName = [NSObject typeForProperty:@"theJumper" inClass:@"MyViewController"];
XCTAssertTrue([typeName isEqualToString:@"<JumperProtocol>"]);
}
【Green腔稀,定義JumperProtocol盆昙,往控制器添加theJumper屬性,讓測試通過】
MyViewController.h文件:
#import <UIKit/UIKit.h>
#import "MyDataSourceProtocol.h"
#import "JumperProtocol.h"
@interface MyViewController : UIViewController
@property (nonatomic, strong) UITableView *theTableView;
@property (nonatomic, strong) id<UITableViewDataSource,UITableViewDelegate,MyDataSourceProtocol> theDataSource;
@property (nonatomic, strong) id<JumperProtocol> theJumper;
@end
JumperProtocol.h文件:
#import <Foundation/Foundation.h>
/**
跳轉(zhuǎn)類與控制器約定的協(xié)議
*/
@protocol JumperProtocol <NSObject>
@end
【Red:tc 5.2焊虏,控制器theJumper屬性是強引用】
/**
tc 5.2
*/
- (void)test_Property_TheJumper_IsStronglyRefered{
@autoreleasepool {
self.theController.theJumper = (id<JumperProtocol>)[[NSObject alloc] init];
}
// weak引用淡喜,會被自動釋放池釋放,強引用不會诵闭。
XCTAssertNotNil(self.theController.theJumper);
}
【Green炼团,當(dāng)前定義的屬性已經(jīng)滿足此測試用例】
二,實現(xiàn)一個專門的cell點擊跳轉(zhuǎn)類疏尿。
【Red瘟芝,tc 5.3 跳轉(zhuǎn)類要實現(xiàn)JumperProtocol協(xié)議】
新建一個關(guān)于跳轉(zhuǎn)類的測試類,添加這個測試用例褥琐。
MyJumperTests.m文件:
/**
tc 5.3
*/
- (void)test_ShouldConformJumperProtocol{
MyCellJumper *jumper = [[MyCellJumper alloc] init];
XCTAssertTrue([jumper conformsToProtocol:@protocol(JumperProtocol)]);
}
【Green锌俱,創(chuàng)建MyCellJumper類并讓它遵循JumperProtocol協(xié)議,讓上面測試用例通過】
MyCellJumper.h文件:
#import "JumperProtocol.h"
@interface MyCellJumper : NSObject <JumperProtocol>
@end
【Red:tc 5.4 跳轉(zhuǎn)類要實現(xiàn)一個接受一個id類型的參數(shù)的跳轉(zhuǎn)方法】
MyJumperTests.m文件:
/**
tc 5.4
*/
- (void)test_Method_ToControllerWithData_ShouldBeImplemented{
MyCellJumper *jumper = [[MyCellJumper alloc] init];
XCTAssertTrue([jumper respondsToSelector:@selector(toControllerWithData:)]);
}
【Green敌呈,給JumperProtocol協(xié)議添加方法- (void)
toControllerWithData:(id)data贸宏,并讓MyCellJumper實現(xiàn)它,讓上面測試用例通過】
JumperProtocol.h文件:
/**
各個模塊的jumper實現(xiàn)這個方法時磕洪,要在方法里面對data做判斷吭练,data是想要的數(shù)據(jù)時,才解析
拿出數(shù)據(jù)析显,執(zhí)行跳轉(zhuǎn)鲫咽。
@param data <#data description#>
*/
- (void)toControllerWithData:(id)data;
MyCellJumper.m文件:
#import "MyCellJumper.h"
@implementation MyCellJumper
#pragma mark - JumperProtocol
- (void)toControllerWithData:(id)data{
}
@end
【Red:tc 5.5,MyCellJumper應(yīng)該實現(xiàn)一個依賴于導(dǎo)航控制器的初始化方法】
MyJumperTests.m文件:
/**
tc 5.5
*/
- (void)test_Method_InitWithNavigationController_ShouldBeImplemented{
MyCellJumper *jumper = [[MyCellJumper alloc] init];
XCTAssertTrue([jumper respondsToSelector:@selector(initWithNavigationController:)]);
}
因為跳轉(zhuǎn)類一定要用到導(dǎo)航控制器叫榕,所以吧這個初始化方法作為協(xié)議必須實現(xiàn)的方法浑侥。
【Green:往JumperProtocol里面添加- (instancetype)initWithNavigationController:(UINavigationController *)navVC方法,并讓MyCellJumper.m實現(xiàn)它】
JumperProtocol.h文件:
/**
這是應(yīng)該被使用的正確的初始化方法晰绎。
1寓落,navVC不能為空。
2荞下,navVC應(yīng)該在內(nèi)部被弱引用伶选。
@param navVC <#navVC description#>
@return <#return value description#>
*/
- (instancetype)initWithNavigationController:(UINavigationController *)navVC;
MyCellJumper.m文件:
- (instancetype)initWithNavigationController:(UINavigationController *)navVC{
return nil;
}
現(xiàn)在一般跳轉(zhuǎn)類所需的公共方法已經(jīng)設(shè)計完成,接下來看怎么實現(xiàn)MyCellJumper這個跳轉(zhuǎn)類的這些方法尖昏,來保證它能夠被正確初始化和實現(xiàn)正確的跳轉(zhuǎn)仰税。
【Red:tc 5.6,MyCellJumper的初始化方法不能傳入空的導(dǎo)航控制器抽诉,否則會觸發(fā)斷言異吃纱兀】
MyJumperTests.m文件:
/**
tc 5.6
*/
- (void)test_ShouldNotPassNilWhenInitWithNavigationController{
XCTAssertThrows([[MyCellJumper alloc] initWithNavigationController:nil]);
}
【Green,在MyCellJumper的初始化方法里面加入判斷導(dǎo)航控制器是否存在的斷言】
MyCellJumper.m文件:
- (instancetype)initWithNavigationController:(UINavigationController *)navVC{
NSAssert(navVC, @"導(dǎo)航控制器不能為nil");
return nil;
}
【Red:tc 5.7迹淌,MyCellJumper對導(dǎo)航控制器的持有應(yīng)該是弱引用】
MyCellJumperTests.m文件:
/**
tc 5.7
*/
- (void)test_NavigationController_ShouldBeWeaklyRefered{
__block MyCellJumper *jumper;
@autoreleasepool {
UINavigationController *navVC = [[UINavigationController alloc] init];
jumper = [[MyCellJumper alloc] initWithNavigationController:navVC];
}
XCTAssertNil(jumper.navigationController);
}
這里碰到了有趣的事情河绽,為了寫上面的測試用例己单,需要MyCellJumper暴露一個導(dǎo)航控制器的引用屬性,這個屬性本可以不暴露的耙饰,但是纹笼,我們?yōu)榱嗽鰪婎惖目蓽y試性,把它暴露出來了苟跪,這種暴露與不暴露是需要平衡的廷痘,畢竟有些情況,暴露的東西多了件已,就破壞了類的封裝性了笋额,而什么都不暴露,類就沒有很好的可測試性拨齐,不利于我們做單元測試鳞陨。這里可以看出測試驅(qū)動開發(fā)的一個好處,即在開發(fā)過程中促使我們?nèi)タ紤]如何讓代碼為測試提供方便瞻惋。畢竟厦滤,若我們不考慮測試性,那么我們的測試用例便寫不下去了歼狼。針對MyCellJumper這個類掏导,我認為暴露一個只讀的指向?qū)Ш娇刂破鞯膶傩允强梢缘模覀儾挥脫?dān)心它會被無意地修改羽峰,也滿足了我們的測試需求趟咆。
【Green:在MyCellJumper類的初始化方法里面,把傳入的導(dǎo)航控制器付給它的navigationController屬性梅屉,這個屬性是weak, readonly修飾的】
MyCellJumper.h文件:
#import "JumperProtocol.h"
@interface MyCellJumper : NSObject <JumperProtocol>
@property (nonatomic, readonly, weak) UINavigationController *navigationController;
@end
MyCellJumper.m文件:
- (instancetype)initWithNavigationController:(UINavigationController *)navVC{
NSAssert(navVC, @"導(dǎo)航控制器不能為nil");
_navigationController = navVC;
return nil;
}
滿足了【tc 5.6值纱,tc 5.7】的初始化方法還不能用,再添加一個測試用例讓它變成真正的初始化方法
【Red:tc 5.8坯汤,MyCellJumper類的初始化方法要返回一個MyCellJumper對象虐唠,對象的navigationController屬性應(yīng)該引用一個導(dǎo)航控制器對象】
MyJumperTests.m文件:
/**
tc 5.8
*/
- (void)test_InitMethod_ShouldReturnASelfTypeInstance_And_Property_navigationController_ShouldReferANavigationControllerInstanceAfterInit{
UINavigationController *navVC = [[UINavigationController alloc] init];
id obj = [[MyCellJumper alloc] initWithNavigationController:navVC];
XCTAssertTrue([obj isKindOfClass:[MyCellJumper class]]);
MyCellJumper *jumper = obj;
XCTAssertTrue([jumper.navigationController isKindOfClass:[UINavigationController class]]);
}
【Green,修改MyCellJumper的初始化方法】
MyCellJumper.m文件:
- (instancetype)initWithNavigationController:(UINavigationController *)navVC{
NSAssert(navVC, @"導(dǎo)航控制器不能為nil");
if (self = [super init]) {
_navigationController = navVC;
}
return self;
}
對MyCellJumper的初始化方法惰聂,我們已經(jīng)用測試用例覆蓋得差不多了疆偿,在接下去開發(fā)跳轉(zhuǎn)方法之前,先對測試代碼執(zhí)行一個Refactor流程搓幌,因為發(fā)現(xiàn)了大部分MyJumperTests.m里面的測試用例的新建一個MyCellJumper對象的代碼可以重用杆故,因此把這部分代碼提取到setUp方法去執(zhí)行。
【Refactor:提取各個測試用例的可重用代碼到setUp方法溉愁,用類成員變量self.jumper對象代替一些測試用例里面的局部變量jumper對象】
MyJumperTests.m文件:
@interface MyJumperTests : XCTestCase
@property (nonatomic, strong) UINavigationController *navVC;
@property (nonatomic, strong) MyCellJumper *jumper;
@end
@implementation MyJumperTests
- (void)setUp {
[super setUp];
self.navVC = [[UINavigationController alloc] init];
self.jumper = [[MyCellJumper alloc] initWithNavigationController:self.navVC];
}
- (void)tearDown {
self.navVC = nil;
self.jumper = nil;
[super tearDown];
}
重構(gòu)后处铛,重新運行所有MyJumperTests.m里面的測試用例,仍然全部通過,說明重構(gòu)沒問題撤蟆。
接下來將針對跳轉(zhuǎn)類真正處理跳轉(zhuǎn)邏輯的核心方法做測試驅(qū)動開發(fā)篙贸。將跳轉(zhuǎn)邏輯封裝進獨立的類來處理,將讓這部分邏輯變得非常有利于做單元測試枫疆。通過給這個類傳參,再觀察它能否對可能情形的參數(shù)做出正確的處理敷鸦,產(chǎn)生正確的目的控制器對象息楔,并執(zhí)行了導(dǎo)航控制器的push方法,我們就能用單元測試用例充分覆蓋到所有需要測試的邏輯扒披。
現(xiàn)在繼續(xù)往MyJumperTests.m里面添加測試用例值依,并繼續(xù)修改MyCellJumper類讓這些測試用例通過。
首先測試跳轉(zhuǎn)方法對異常情況的處理碟案,當(dāng)傳入nil和非MyModel類型的參數(shù)時它不執(zhí)行任跳轉(zhuǎn)愿险。
如何驗證不執(zhí)行跳轉(zhuǎn)?其實就是驗證導(dǎo)航控制器沒有調(diào)用push方法。所以這里要用到文章【四】里面創(chuàng)建的FakeNavigationViewController來代替真實的導(dǎo)航控制器來跟跳轉(zhuǎn)類交互价说,因為唯有在假導(dǎo)航控制器對象里面辆亏,我們經(jīng)過了可測試處理后,才能感知push方法是否執(zhí)行了鳖目。其實這里也不能說它是假對象扮叨,比較它是真導(dǎo)航控制器的子類,能執(zhí)行真正的push方法领迈,更準(zhǔn)確的說法是它是一個可測試性的導(dǎo)航控制器對象彻磁。無論是用假對象,或可測試對象來替換產(chǎn)品代碼里面原有的對象狸捅,都是用了同樣的測試技術(shù)衷蜓,將被測對象與其依賴的對象隔離開來,用我們設(shè)計好的假對象來替換這些依賴的對象尘喝,然后我們就可以通過感知到假對象與被測對象交互過程中發(fā)生的變化來測試被測對象的外資行為磁浇。至于怎么創(chuàng)建假對象來實現(xiàn)這種隔離,一般有通過接口隔離瞧省,通過子類替換扯夭,通過方法替換等技術(shù)方法。這里使用的就是通過子類替換的方法鞍匾。
【tc 5.9交洗,保證跳轉(zhuǎn)方法傳nil時不跳轉(zhuǎn)】
【tc 5.10,保證跳轉(zhuǎn)方法傳非MyModel類型參數(shù)時不跳轉(zhuǎn)】
MyJumperTests.m文件:
/**
tc 5.9
*/
- (void)test_Method_ToControllerWithData_DoNotPushWithNil{
__block NSString *calledMethod;
FakeNavigationViewController *nav = [[FakeNavigationViewController alloc] init];
nav.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
calledMethod = methodName;
};
MyCellJumper *jumper = [[MyCellJumper alloc] initWithNavigationController:nav];
[jumper toControllerWithData:nil];
XCTAssertNil(calledMethod);
}
/**
tc 5.10
*/
- (void)test_Method_ToControllerWithData_DoNotPushWithNotMyModelTypeData{
__block NSString *calledMethod;
FakeNavigationViewController *nav = [[FakeNavigationViewController alloc] init];
nav.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
calledMethod = methodName;
};
MyCellJumper *jumper = [[MyCellJumper alloc] initWithNavigationController:nav];
NSObject *otherPara = [[NSObject alloc] init];
[jumper toControllerWithData:otherPara];
XCTAssertNil(calledMethod);
}
不需要對現(xiàn)有MyCellJumper代碼做任何改動橡淑,這兩個測試用例也會通過构拳,因為跳轉(zhuǎn)方法還什么都沒做呢。
然后測試跳轉(zhuǎn)方法在傳入MyModel類型參數(shù)時能否實現(xiàn)正確的跳轉(zhuǎn)。
【Red:tc 5.11置森,測試當(dāng)model數(shù)據(jù)類型為A類型時斗埂,要跳轉(zhuǎn)到A類型指定控制器】
MyJumperTests.m文件:
/**
tc 5.11
*/
- (void)test_JumpToATypeViewController_WithATypeData{
__block NSString *calledMethod;
__block UIViewController *controller;
FakeNavigationViewController *nav = [[FakeNavigationViewController alloc] init];
nav.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
calledMethod = methodName;
controller = parameters[[FakeNavigationViewController pushControllerParaKey]];
};
MyCellJumper *jumper = [[MyCellJumper alloc] initWithNavigationController:nav];
NSObject *otherPara = [[NSObject alloc] init];
[jumper toControllerWithData:otherPara];
XCTAssertTrue([calledMethod isEqualToString:[FakeNavigationViewController pushMethodName]]);
XCTAssertTrue([controller isKindOfClass:[ATypeViewController class]]);
}
【Green:往跳轉(zhuǎn)方法里面實現(xiàn)對A類型數(shù)據(jù)的跳轉(zhuǎn)邏輯】
MyCellJumper.m文件:
- (void)toControllerWithData:(id)data{
MyModel *model = data;
if (model.type == ModelTypeA) {
ATypeViewController *vc = [[ATypeViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}
}
運行MyJumperTests.m里面所有測試用例,【tc 5.11】通過了凫海,但是【tc 5.9呛凶,tc 5.10】失敗了。
雖然前面【tc 5.9行贪,tc 5.10】一開始不用做任何代碼修改它們就通過了漾稀,感覺沒什么用處,但此刻它們起到了捕獲bugs的作用建瘫,它們分別揭示了當(dāng)前跳轉(zhuǎn)方法的實現(xiàn)的兩個問題:1崭捍,傳參為nil時也能滿足model.type == ModelTypeA的條件;2啰脚,傳參為非nil非MyModel類型數(shù)據(jù)時將會因為unrecognized selector
問題發(fā)生崩潰殷蛇。
我們繼續(xù)完善跳轉(zhuǎn)方法的實現(xiàn),修復(fù)著兩個bugs橄浓。
MyCellJumper.m文件:
- (void)toControllerWithData:(id)data{
if (!data || ![data isKindOfClass:[MyModel class]]) {
return;
}
MyModel *model = data;
if (model.type == ModelTypeA) {
ATypeViewController *vc = [[ATypeViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}
}
終于粒梦,現(xiàn)在我們讓所有測試用例都Green了,這感覺真棒荸实!
接下來還有對B類型谍倦、C類型數(shù)據(jù)的跳轉(zhuǎn)的測試用例需要添加,不過在進一步測試之前泪勒,我們又發(fā)現(xiàn)了這是可以進行一次Refactor流程的好時機昼蛀,因為【tc 5.9,tc 5.10圆存,tc 5.11】之間有不少冗余代碼可以清理叼旋。
【Refactor:清理測試用例冗余代碼,讓它們更簡潔】
將冗余代碼放入setUp文件沦辙。
MyJumperTests.m文件:
@interface MyJumperTests : XCTestCase
@property (nonatomic, strong) UINavigationController *navVC;
@property (nonatomic, strong) FakeNavigationViewController *fakeNavVC;
@property (nonatomic, strong) MyCellJumper *jumper;
@property (nonatomic, strong) MyCellJumper *jumperWithFakeNavVC;
@property (nonatomic, strong) UIViewController *pushedController;
@property (nonatomic, strong) NSString *pushMethod;
@end
@implementation MyJumperTests
- (void)setUp {
[super setUp];
// 依賴于可測試導(dǎo)航欄控制器的jumper
self.fakeNavVC = [[FakeNavigationViewController alloc] init];
__weak typeof(self) wSelf = self;
self.fakeNavVC.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
__strong typeof(self) sSelf = wSelf;
sSelf.pushMethod = methodName;
sSelf.pushedController = parameters[[FakeNavigationViewController pushControllerParaKey]];
};
self.jumperWithFakeNavVC = [[MyCellJumper alloc] initWithNavigationController:self.fakeNavVC];
// 正常的jumper
self.navVC = [[UINavigationController alloc] init];
self.jumper = [[MyCellJumper alloc] initWithNavigationController:self.navVC];
}
- (void)tearDown {
self.navVC = nil;
self.jumper = nil;
self.pushedController = nil;
self.pushMethod = nil;
[super tearDown];
}
【tc 5.9夫植,tc 5.10,tc 5.11】由原來的一長串代碼變成很少的幾行代碼油讯,而且可以預(yù)期详民,接下來新增的兩個數(shù)據(jù)類型跳轉(zhuǎn)的測試用例也仍然是幾行代碼。
MyJumperTests.m文件:
/**
tc 5.9
*/
- (void)test_Method_ToControllerWithData_DoNotPushWithNil{
[self.jumperWithFakeNavVC toControllerWithData:nil];
XCTAssertNil(self.pushMethod);
}
/**
tc 5.10
*/
- (void)test_Method_ToControllerWithData_DoNotPushWithNotMyModelTypeData{
NSObject *otherPara = [[NSObject alloc] init];
[self.jumperWithFakeNavVC toControllerWithData:otherPara];
XCTAssertNil(self.pushMethod);
}
/**
tc 5.11
*/
- (void)test_JumpToATypeViewController_WithATypeData{
MyModel *model = [[MyModel alloc] init];
model.type = ModelTypeA;
[self.jumperWithFakeNavVC toControllerWithData:model];
XCTAssertTrue([self.pushMethod isEqualToString:[FakeNavigationViewController pushMethodName]]);
XCTAssertTrue([self.pushedController isKindOfClass:[ATypeViewController class]]);
}
這次Refactor效果不錯陌兑,在很好地減少了測試代碼的同時沈跨,讓測試用例的測試意圖表達得更簡潔直觀了。
現(xiàn)在開始添加對B兔综、C類型饿凛,和其他類型的數(shù)據(jù)的跳轉(zhuǎn)邏輯狞玛,完成我們的跳轉(zhuǎn)方法的測試開發(fā)。
【Red:tc 5.12涧窒,測試保證B類型數(shù)據(jù)跳轉(zhuǎn)到B類型指定控制器】
MyJumperTests.m文件:
/**
tc 5.12
*/
- (void)test_JumpToBTypeViewController_WithBTypeData{
MyModel *model = [[MyModel alloc] init];
model.type = ModelTypeB;
[self.jumperWithFakeNavVC toControllerWithData:model];
XCTAssertTrue([self.pushMethod isEqualToString:[FakeNavigationViewController pushMethodName]]);
XCTAssertTrue([self.pushedController isKindOfClass:[BTypeViewController class]]);
}
/**
tc 5.13
*/
- (void)test_JumpToCTypeViewController_WithCTypeData{
MyModel *model = [[MyModel alloc] init];
model.type = ModelTypeC;
[self.jumperWithFakeNavVC toControllerWithData:model];
XCTAssertTrue([self.pushMethod isEqualToString:[FakeNavigationViewController pushMethodName]]);
XCTAssertTrue([self.pushedController isKindOfClass:[CTypeViewController class]]);
}
/**
tc 5.14
*/
- (void)test_DoNotPushWhenMyModelTypeDataWithOtherTypeValue{
MyModel *model = [[MyModel alloc] init];
model.type = 100;
[self.jumperWithFakeNavVC toControllerWithData:model];
XCTAssertNil(self.pushMethod);
XCTAssertNil(self.pushedController);
}
【Green:新建BTypeViewController心肪、CTypeViewController類,修改跳轉(zhuǎn)方法實現(xiàn)】
MyCellJumper.m文件:
- (void)toControllerWithData:(id)data{
if (!data || ![data isKindOfClass:[MyModel class]]) {
return;
}
MyModel *model = data;
UIViewController *vc;
switch (model.type) {
case ModelTypeA:
vc = [[ATypeViewController alloc] init];
break;
case ModelTypeB:
vc = [[BTypeViewController alloc] init];
break;
case ModelTypeC:
vc = [[CTypeViewController alloc] init];
break;
default:{
return;
}
break;
}
[self.navigationController pushViewController:vc animated:YES];
}
至此纠吴,跳轉(zhuǎn)方法已經(jīng)測試開發(fā)完成硬鞍,同時,這個cell的專門跳轉(zhuǎn)類也已經(jīng)開發(fā)測試完成戴已,下一步膳凝,就是要在控制器里面使用它,看能不能達到我們將控制器與跳轉(zhuǎn)邏輯解耦的目的恭陡。
三,用跳轉(zhuǎn)類在控制器里面實現(xiàn)cell的跳轉(zhuǎn)邏輯上煤。
首先要修改數(shù)據(jù)源代理類MyTableViewDataSource的cellTapBlock休玩,讓它傳遞一個id類型的參數(shù)用來給跳轉(zhuǎn)類MyCellJumper接收。原來有一個相關(guān)的測試用例【tc 4.6】劫狠,它當(dāng)前測試的是cell被tapped后是否將cell的row通過cellTapBlock傳遞了出去拴疤,我們現(xiàn)在修改讓它傳遞cell的數(shù)據(jù)模型。
【Red:tc 4.6独泞,修改為表格數(shù)據(jù)源代理類在cell被點擊時應(yīng)該要將cell對應(yīng)的數(shù)據(jù)model傳遞給外界】
這是對數(shù)據(jù)源代理類的修改呐矾,所以測試用例放在它對應(yīng)的測試類里面。
MyTableViewDataSourceTests.m文件:
/**
tc 4.6
*/
- (void)test_CellTapBlockReceiveDataOfTappedCell{
self.dataSource.theDataArray = @[@{@"type":@0,@"title":@"Type A Title",@"someId":@"0001"},@{@"type":@1,@"title":@"Type B Title",@"someId":@"0002"}];
__block id model;
self.dataSource.cellTapBlock = ^(id dataModel){
model = dataModel;
};
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:1 inSection:0];
[self.dataSource tableView:self.theTableView didSelectRowAtIndexPath:indexPath];
XCTAssertNotNil(model);
XCTAssertTrue([model isKindOfClass:[MyModel class]]);
MyModel *cellModel = model;
XCTAssertTrue([cellModel.someId isEqualToString:@"0002"]);
XCTAssertTrue([cellModel.title isEqualToString:@"Type B Title"]);
XCTAssertTrue(cellModel.type == ModelTypeB);
}
顯而易見的懦砂,我發(fā)現(xiàn)另一個測試用例【tc 4.5】也得做響應(yīng)的修改蜒犯,把cellTapBlock的參數(shù)改為id類型。
MyTableViewDataSourceTests.m文件:
/**
tc 4.5
*/
- (void)test_ExecuteCellTapBlockIfCellSelectedMethodCalled{
__block BOOL called = NO;
self.dataSource.cellTapBlock = ^(id dataModel){
called = YES;
};
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.dataSource tableView:self.theTableView didSelectRowAtIndexPath:indexPath];
XCTAssertTrue(called);
}
做完這些改動荞膘,我這次沒有全部運行一次所有測試用例罚随,我就先去改產(chǎn)品代碼了。
【Green:修改數(shù)據(jù)源代理類與控制器之間的交互協(xié)議羽资,修改數(shù)據(jù)源代理類的cell選擇代理方法的實現(xiàn)】
在MyDataSourceProtocol.h文件里面修改cellTapBlock的參數(shù):
@protocol MyDataSourceProtocol <NSObject>
@optional
@property (nonatomic, strong) NSArray *theDataArray;
@property (nonatomic, copy) void(^updateBlock)();
@property (nonatomic, copy) void(^cellTapBlock)(id dataModel);
@end
在MyTableViewDataSource.m文件里面修改cell選擇的代理方法的實現(xiàn):
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (self.cellTapBlock) {
MyModel *model = [[MyModel alloc] init];
NSDictionary *data = self.theDataArray[indexPath.row];
model.someId = data[@"someId"];
model.title = data[@"title"];
model.type = [data[@"type"] integerValue];
self.cellTapBlock(model);
}
}
然后淘菩,執(zhí)行一次整個工程的所有測試用例,以上的兩個測試用例重新通過了屠升,但是發(fā)現(xiàn)了控制器的測試用例里面有一個失敗了潮改。
反正我們也要開始改控制器的代碼了,就把這個測試用例作為我們這次對控制器修改的第一個Red流程吧腹暖。只不過汇在,它的命名和實現(xiàn)都要修改一番,以表達我們新的測試意圖脏答。原來我們的做法是通過讓控制器調(diào)用數(shù)據(jù)源代理類的cell選擇代理方法趾疚,然后檢測控制器的導(dǎo)航控制器屬性(通過用Fake替換真實的導(dǎo)航欄控制器的方法來感知)是否拿到了正確的要被pushed的控制器對象缨历,是否執(zhí)行了push方法,來驗證cell的選擇事件是否導(dǎo)致了正確的push行為〔诼螅現(xiàn)在我們不用這么做了辛孵,因為,我們已經(jīng)在MyJumperTests.m里面對跳轉(zhuǎn)類的跳轉(zhuǎn)邏輯進行了單元測試覆蓋赡磅,而且都測試通過魄缚,說明了MyJumper類是可靠的,在控制器這邊的跳轉(zhuǎn)邏輯測試焚廊,我們只需要測試在cell被選擇后冶匹,控制器的theJumper對象是否執(zhí)行了跳轉(zhuǎn)方法,以及它的跳轉(zhuǎn)方法是否拿到了正確的參數(shù)即可咆瘟。那么如何感知theJumper對象是否執(zhí)行了方法嚼隘,拿到了正確的參數(shù)?我們是不是繼續(xù)像前面做法一樣通過FakeNavigationViewController來感知它是否執(zhí)行了push和拿到了要push的控制器對象袒餐?不飞蛹,因為現(xiàn)在我們要測的是控制器,跳轉(zhuǎn)邏輯部分是MyJumper類對象直接與控制器打交道灸眼,導(dǎo)航控制器對象如何被操作屬于MyJumper的實現(xiàn)細節(jié)了卧檐,它離我們的測試目標(biāo)比較遠,我們不應(yīng)該讓它做感知對象焰宣。而應(yīng)該找一個對象霉囚,替換直接與控制器打交道的MyJumper類對象,來作為對控制器行為的感知對象匕积。只要想到要通過替換對象的方式來做測試盈罐,通常就想到要創(chuàng)建一個假對象或者一個可測試的對象,這種對象的創(chuàng)建方法要么是通過創(chuàng)建子類闪唆,要么是通過實現(xiàn)協(xié)議暖呕,這里因為theJumper屬性與控制器之間是通過協(xié)議來交互的,所以苞氮,我們創(chuàng)建一個跟theJumper屬性實現(xiàn)同樣協(xié)議的對象來替換真實的MyJumper類對象湾揽,作為控制器的theJumper屬性,然后在測試用例里面使用它來感知控制器是否正確地使用了它笼吟。
【Red:tc 4.8库物,修改控制器的這個測試用例為在數(shù)據(jù)源代理類對象在響應(yīng)了cell的選擇代理方法后,跳轉(zhuǎn)類對象是否調(diào)用了跳轉(zhuǎn)方法】
MyViewControllerTests.m文件:
/**
tc 4.8
*/
- (void)test_SelectACellTheJumperCallJumpMethod{
UINavigationController *navVC = [[UINavigationController alloc] init];
FakeMyJumper *jumper = [[FakeMyJumper alloc] initWithNavigationController:navVC];
__block NSString *name;
jumper.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
name = methodName;
};
self.theController.theJumper = jumper;
self.theController.theDataSource = self.theDataSource;
[self.theController viewDidLoad];
[self.theDataSource tableView:self.theTableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
XCTAssertTrue([name isEqualToString:[FakeMyJumper jumpMethodName]]);
}
【Green:創(chuàng)建FakeMyJumper類贷帮,修改控制器viewDidLoad里面處理跳轉(zhuǎn)的邏輯】
FakeMyJumper要替換MyJumper類戚揭,就得實現(xiàn)跟它一樣實現(xiàn)JumperProtocol協(xié)議;同時要具備可測試性就得導(dǎo)入NSObject+TestingHelper類別撵枢,在要檢測的方法里面民晒,執(zhí)行callMethodBlock精居。
FakeMyJumper.h文件:
#import "JumperProtocol.h"
#import "NSObject+TestingHelper.h"
@interface FakeMyJumper : NSObject <JumperProtocol>
/**
獲取常亮的方法名字符串,方便用來做檢測對比潜必。
@return <#return value description#>
*/
+ (NSString *)jumpMethodName;
@end
FakeMyJumper.m文件:
#import "FakeMyJumper.h"
@implementation FakeMyJumper
#pragma mark - JumperProtocol
- (void)toControllerWithData:(id)data{
// 調(diào)用了本方法靴姿,外界就能檢測到
if (self.callMethodBlock) {
self.callMethodBlock([FakeMyJumper jumpMethodName], nil);
}
}
- (instancetype)initWithNavigationController:(UINavigationController *)navVC{
return self;
}
+ (NSString *)jumpMethodName{
return @"toControllerWithData:";
}
@end
控制器的viewDidLoad方法里面處理跳轉(zhuǎn)的邏輯將由原來的這種硬編碼:
MyViewController.m文件:
- (void)viewDidLoad {
// 其他代碼。磁滚。佛吓。
// cell跳轉(zhuǎn)邏輯
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:YES];
}
};
// 其他代碼。垂攘。维雇。
}
變成這樣的依賴協(xié)議的簡潔代碼:
MyViewController.m文件:
- (void)viewDidLoad {
// 其他代碼。晒他。吱型。
// cell跳轉(zhuǎn)邏輯
self.theDataSource.cellTapBlock = ^(id dateModel) {
__strong typeof(self) sSelf = wSelf;
if (sSelf.theJumper) {
[sSelf.theJumper toControllerWithData:dateModel];
}
};
// 其他代碼。陨仅。津滞。
}
【Red:tc 5.15,當(dāng)選擇A類型的cell時掂名,跳轉(zhuǎn)類對象獲得A類型的model數(shù)據(jù)】
MyViewControllerTests.m文件:
/**
tc 5.15
*/
- (void)test_SelectATypeCellPassATypeModelToTheJumper{
UINavigationController *navVC = [[UINavigationController alloc] init];
FakeMyJumper *jumper = [[FakeMyJumper alloc] initWithNavigationController:navVC];
__block NSString *name;
__block id model;
jumper.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
name = methodName;
model = parameters[[FakeMyJumper modelKey]];
};
self.theController.theJumper = jumper;
self.theDataSource.theDataArray = @[@{@"type":@0,@"title":@"Type A Title",@"someId":@"0001"},@{@"type":@1,@"title":@"Type B Title",@"someId":@"0002"}];
self.theController.theDataSource = self.theDataSource;
[self.theController viewDidLoad];
[self.theDataSource tableView:self.theTableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
XCTAssertTrue([name isEqualToString:[FakeMyJumper jumpMethodName]]);
XCTAssertTrue([model isKindOfClass:[MyModel class]]);
MyModel *dataModel = model;
XCTAssertTrue(dataModel.type == ModelTypeA);
}
【Green:修改FakeMyJumper,讓它可以將接收的參數(shù)通過block傳遞出來哟沫;修改控制器饺蔑,去掉#import "ATypeViewController.h"
】
FakeMyJumper.h文件,添加獲取參數(shù)字典key的方法:
/**
從參數(shù)字典里面獲取model對象的key
@return <#return value description#>
*/
+ (NSString *)modelKey;
FakeMyJumper.m文件嗜诀,調(diào)用方法時猾警,把參數(shù)傳入block:
- (void)toControllerWithData:(id)data{
// 調(diào)用了本方法,外界就能檢測到
if (self.callMethodBlock) {
self.callMethodBlock([FakeMyJumper jumpMethodName], @{[FakeMyJumper modelKey]:data});
}
}
+ (NSString *)modelKey{
return @"dataModel";
}
重新運行所有測試隆敢,全部通過发皿。繼續(xù)添加對B, C類型cell的跳轉(zhuǎn)測試。
【Red:tc 5.16拂蝎,當(dāng)選擇B類型的cell時穴墅,跳轉(zhuǎn)類對象獲得B類型的model數(shù)據(jù)】
MyViewControllerTests.m文件:
/**
tc 5.16
*/
- (void)test_SelectBTypeCellPassBTypeModelToTheJumper{
UINavigationController *navVC = [[UINavigationController alloc] init];
FakeMyJumper *jumper = [[FakeMyJumper alloc] initWithNavigationController:navVC];
__block NSString *name;
__block id model;
jumper.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
name = methodName;
model = parameters[[FakeMyJumper modelKey]];
};
self.theController.theJumper = jumper;
self.theDataSource.theDataArray = @[@{@"type":@0,@"title":@"Type A Title",@"someId":@"0001"},@{@"type":@1,@"title":@"Type B Title",@"someId":@"0002"}];
self.theController.theDataSource = self.theDataSource;
[self.theController viewDidLoad];
[self.theDataSource tableView:self.theTableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]];
XCTAssertTrue([name isEqualToString:[FakeMyJumper jumpMethodName]]);
XCTAssertTrue([model isKindOfClass:[MyModel class]]);
MyModel *dataModel = model;
XCTAssertTrue(dataModel.type == ModelTypeB);
}
/**
tc 5.17
*/
- (void)test_SelectCTypeCellPassCTypeModelToTheJumper{
UINavigationController *navVC = [[UINavigationController alloc] init];
FakeMyJumper *jumper = [[FakeMyJumper alloc] initWithNavigationController:navVC];
__block NSString *name;
__block id model;
jumper.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
name = methodName;
model = parameters[[FakeMyJumper modelKey]];
};
self.theController.theJumper = jumper;
self.theDataSource.theDataArray = @[@{@"type":@0,@"title":@"Type A Title",@"someId":@"0001"},@{@"type":@1,@"title":@"Type B Title",@"someId":@"0002"},@{@"type":@2,@"title":@"Type C Title",@"someId":@"0003"}];
self.theController.theDataSource = self.theDataSource;
[self.theController viewDidLoad];
[self.theDataSource tableView:self.theTableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]];
XCTAssertTrue([name isEqualToString:[FakeMyJumper jumpMethodName]]);
XCTAssertTrue([model isKindOfClass:[MyModel class]]);
MyModel *dataModel = model;
XCTAssertTrue(dataModel.type == ModelTypeC);
}
【Green:不用修改任何產(chǎn)品代碼,這兩個測試用例就通過了】
留意到【tc 5.15温自,tc 5.16玄货,tc 5.17】有很多冗余的代碼,所以又到了可以執(zhí)行Refactor流程的點悼泌。
【Refactor:整理測試代碼松捉,消除冗余】
提取公共方法,將測試用例常用的變量作為測試類的成員變量馆里。
MyViewControllerTests.m文件:
@interface MyViewControllerTests : XCTestCase
@property (nonatomic, strong) UITableView *theTableView;
@property (nonatomic, strong) MyTableViewDataSource *theDataSource;
@property (nonatomic, strong) MyViewController *theController;
@property (nonatomic, strong) FakeMyJumper *fakeJumper;
@property (nonatomic, copy) NSString *cellJumpMethod;
@property (nonatomic, strong) id dataPassedToJumper;
@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;
self.fakeJumper = nil;
self.cellJumpMethod = nil;
self.dataPassedToJumper = nil;
[super tearDown];
}
/**
選擇一種數(shù)據(jù)類型的cell
@param type <#type description#>
*/
- (void)selectCellWithDataType:(ModelType)type{
UINavigationController *navVC = [[UINavigationController alloc] init];
self.fakeJumper = [[FakeMyJumper alloc] initWithNavigationController:navVC];
__weak typeof(self) wSelf = self;
self.fakeJumper.callMethodBlock = ^(NSString *methodName, NSDictionary *parameters) {
__strong typeof(self) sSelf = wSelf;
sSelf.cellJumpMethod = methodName;
sSelf.dataPassedToJumper = parameters[[FakeMyJumper modelKey]];
};
self.theController.theJumper = self.fakeJumper;
self.theDataSource.theDataArray = @[@{@"type":@0,@"title":@"Type A Title",@"someId":@"0001"},@{@"type":@1,@"title":@"Type B Title",@"someId":@"0002"},@{@"type":@2,@"title":@"Type C Title",@"someId":@"0003"}];
self.theController.theDataSource = self.theDataSource;
[self.theController viewDidLoad];
NSInteger row = 0;
if (type == ModelTypeA) {
row = 0;
}else if (type == ModelTypeB){
row = 1;
}else if (type == ModelTypeC){
row = 2;
}
[self.theDataSource tableView:self.theTableView didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:0]];
}
【tc4.8隘世,tc 5.15可柿,tc 5.16,tc 5.17】幾個測試用例將變得很簡潔丙者,而且測試意圖也更明顯:
MyViewControllerTests.m文件:
/**
tc 4.8
*/
- (void)test_SelectACellTheJumperCallJumpMethod{
[self selectCellWithDataType:ModelTypeC];
XCTAssertTrue([self.cellJumpMethod isEqualToString:[FakeMyJumper jumpMethodName]]);
}
/**
tc 5.15
*/
- (void)test_SelectATypeCellPassATypeModelToTheJumper{
[self selectCellWithDataType:ModelTypeA];
XCTAssertTrue([self.cellJumpMethod isEqualToString:[FakeMyJumper jumpMethodName]]);
XCTAssertTrue([self.dataPassedToJumper isKindOfClass:[MyModel class]]);
MyModel *dataModel = self.dataPassedToJumper;
XCTAssertTrue(dataModel.type == ModelTypeA);
}
/**
tc 5.16
*/
- (void)test_SelectBTypeCellPassBTypeModelToTheJumper{
[self selectCellWithDataType:ModelTypeB];
XCTAssertTrue([self.cellJumpMethod isEqualToString:[FakeMyJumper jumpMethodName]]);
XCTAssertTrue([self.dataPassedToJumper isKindOfClass:[MyModel class]]);
MyModel *dataModel = self.dataPassedToJumper;
XCTAssertTrue(dataModel.type == ModelTypeB);
}
/**
tc 5.17
*/
- (void)test_SelectCTypeCellPassCTypeModelToTheJumper{
[self selectCellWithDataType:ModelTypeC];
XCTAssertTrue([self.cellJumpMethod isEqualToString:[FakeMyJumper jumpMethodName]]);
XCTAssertTrue([self.dataPassedToJumper isKindOfClass:[MyModel class]]);
MyModel *dataModel = self.dataPassedToJumper;
XCTAssertTrue(dataModel.type == ModelTypeC);
}
運行全部測試用例复斥,沒有一個失敗,說明這次重構(gòu)沒問題蔓钟。
這篇文章到這里就結(jié)束了永票,現(xiàn)在產(chǎn)品代碼的控制器里面沒有關(guān)聯(lián)MyModel類,沒有關(guān)聯(lián)ATypeViewController滥沫,BTypeViewController侣集,CTypeViewController類,但是卻能夠根據(jù)選擇不同的cell兰绣,執(zhí)行不同的跳轉(zhuǎn)世分。所以,到現(xiàn)在為止缀辩,我們的控制器已經(jīng)跟我們模塊專有的cell的跳轉(zhuǎn)邏輯解耦了臭埋,它里面不會存有跟模塊相關(guān)的專門的cell跳轉(zhuǎn)的業(yè)務(wù)邏輯代碼了,我實現(xiàn)了篇頭所說的我想要的重構(gòu)方案臀玄。這種重構(gòu)方案不僅讓產(chǎn)品代碼具備更好的設(shè)計性瓢阴,而且讓產(chǎn)品重要的業(yè)務(wù)邏輯變得更容易測試。
待續(xù)健无。荣恐。。累贤。
demo:
https://github.com/zard0/TDDListModuleDemo.git