第【四】篇的最后,我說道我碰到了一個令人糾結(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