Model的開發(fā)經(jīng)過了文章【一】后茴恰,我們先告一段落巴粪,現(xiàn)在來想想怎么開發(fā)MVC的V和C部分哨鸭。V的部分我們用現(xiàn)成的UITableView民宿,所以接下來重點關注C的部分。
嘗試去開發(fā)Controller類
除了需求【5】之外像鸡,其他的需求都跟Controller相關活鹰,從數(shù)據(jù)的獲取、封裝只估、顯示到控制跳轉(zhuǎn)志群,看起來Controller就會是一個比較多代碼的類了。要在Controller里面測試所以上述功能蛔钙,那么Controller需要暴露很多公共方法和屬性锌云,這樣Controller就比較難看了,而且也不具備好的封裝性吁脱。所以桑涎,我的策略是將一些相對可以獨立的功能單獨封裝成類,然后分別測試它們兼贡,最后測試它們的交互是否正確攻冷,通過這種先肢解在合并的方式來測試和開發(fā)Controller。哪些功能最適合劃分獨立的類來處理呢遍希,很容易想到就是UITableView的數(shù)據(jù)源和代理類等曼,網(wǎng)絡請求類,我們先從這兩個類下手凿蒜,其他的禁谦,后續(xù)有需求在處理。
(一)開發(fā)表格視圖的數(shù)據(jù)源和代理類
這個類應該實現(xiàn)<UITableViewDataSource,UITableViewDelegate>這兩個代理的下面幾個方法:提供行數(shù)和Cell的數(shù)據(jù)源方法废封;提供行高和行被點擊的代理方法枷畏。
繼續(xù)我們之前的做法,沒寫產(chǎn)品代碼之前虱饿,先寫測試用例。創(chuàng)建一個針對這個類的測試類MyTableViewDataSourceTests,添加第一個測試用例測試它是否遵循了UITableViewDataSource協(xié)議氮发。
【tc 2.1.1】
- (void)testConformUITableViewDelegateProtocol{
MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
XCTAssertTrue([dataSource conformsToProtocol:@protocol(UITableViewDataSource)]);
}
一開始它沒有編譯通過
我們創(chuàng)建產(chǎn)品類MyTableViewDataSource渴肉,讓編譯成功,并讓這個測試通過
遵循了協(xié)議顯然還不夠爽冕,我們需要它確實去實現(xiàn)了我們想要的協(xié)議方法仇祭,新增幾個測試它是否實現(xiàn)了返回行數(shù)和Cell的測試用例。
【tc 2.1.2颈畸,tc 2.1.3】
- (void)test_ImplMethod_tableView_numberOfRowsInSection{
MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
XCTAssertTrue([dataSource respondsToSelector:@selector(tableView: numberOfRowsInSection:)]);
}
- (void)test_ImplMethod_tableView_cellForRowAtIndexPath{
MyTableViewDataSource *dataSource = [[MyTableViewDataSource alloc] init];
XCTAssertTrue([dataSource respondsToSelector:@selector(tableView: cellForRowAtIndexPath:)]);
}
顯然一開始它們沒能通過乌奇,因為類只遵循了協(xié)議,未實現(xiàn)協(xié)議方法眯娱,這是一個Red流程礁苗。
我們執(zhí)行Green流程,讓這兩個測試通過徙缴。
#import "MyTableViewDataSource.h"
@implementation MyTableViewDataSource
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 3;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
return [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"UITableViewCell"];
}
@end
甭管我們的產(chǎn)品代碼實現(xiàn)是否合理试伙,總之做到能讓我們的測試用例通過,就可以了于样。
現(xiàn)在我們成功執(zhí)行了Green流程疏叨,也發(fā)現(xiàn)了測試代碼里面有可以重構的地方,所以我們執(zhí)行一下Refactor流程穿剖,把測試用例里面用到的重復代碼提取到setUp方法里面蚤蔓。
重構后的測試代碼:
#import <XCTest/XCTest.h>
#import "MyTableViewDataSource.h"
@interface MyTableViewDataSourceTests : XCTestCase
@property (nonatomic, strong) MyTableViewDataSource *dataSource;
@end
@implementation MyTableViewDataSourceTests
- (void)setUp {
[super setUp];
self.dataSource = [[MyTableViewDataSource alloc] init];
}
- (void)tearDown {
self.dataSource = nil;
[super tearDown];
}
- (void)testConformUITableViewDelegateProtocol{
XCTAssertTrue([self.dataSource conformsToProtocol:@protocol(UITableViewDataSource)]);
}
- (void)test_ImplMethod_tableView_numberOfRowsInSection{
XCTAssertTrue([self.dataSource respondsToSelector:@selector(tableView: numberOfRowsInSection:)]);
}
- (void)test_ImplMethod_tableView_cellForRowAtIndexPath{
XCTAssertTrue([self.dataSource respondsToSelector:@selector(tableView: cellForRowAtIndexPath:)]);
}
@end
重構完成后,我們要確保所有測試用例依然通過糊余。
對UITableViewDelegate的測試方法一樣秀又,不再贅述。到此啄刹,表格數(shù)據(jù)源代理類的開發(fā)測試先告一段落涮坐。
(二)開發(fā)實現(xiàn)讓數(shù)據(jù)源代理類為表格提供數(shù)據(jù)
這部分代碼是在控制器里面實現(xiàn)的,這部分功能的測試任務是要測試控制器誓军、表格視圖和表格數(shù)據(jù)源代理類這幾個類是否協(xié)作正確袱讹。保證了它們協(xié)作正確,這部分的單元測試任務就算完成了昵时,至于表格看不看得到數(shù)據(jù)捷雕,看到的數(shù)據(jù)是怎樣的,其實這不是單元測試的任務了壹甥,應該由UI測試去覆蓋救巷。
怎么去測試這些類的協(xié)作呢,大概分為幾個部分:
(1)確認控制器有表格視圖句柠、數(shù)據(jù)源代理類的存在浦译。
(2)確認控制器將數(shù)據(jù)源代理類成功賦值給表格作為其數(shù)據(jù)源和代理棒假。
(3)確認表格視圖的行數(shù)、行高和Cell跟其數(shù)據(jù)源代理類提供的數(shù)據(jù)一致精盅。
首先帽哑,寫測試用例去測試(1)
測試先行,創(chuàng)建針對控制器的測試類MyViewControllerTests叹俏,先寫會失敗的測試用例妻枕,執(zhí)行Red流程。
【tc 2.2.1粘驰,測試控制器是否存在一個屬性來引用表格控制器】
#import <XCTest/XCTest.h>
#import <objc/runtime.h>
@interface MyViewControllerTests : XCTestCase
@end
@implementation MyViewControllerTests
- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}
/**
tc 2.2.1
*/
- (void)test_PropertyExist_TheTableView{
objc_property_t theTableViewProperty = class_getProperty([MyViewController class], "theTableView");
XCTAssertTrue(theTableViewProperty != NULL);
}
@end
再執(zhí)行Green流程屡谐,讓這個測試通過
#import <UIKit/UIKit.h>
@interface MyViewController : UIViewController
@property (nonatomic, strong) NSObject *theTableView;
@end
很明顯,我們只要給控制器一個名叫theTableView的屬性蝌数,這個測試用例就會通過愕掏,不管屬性的類型是什么。這不是我們想要的結(jié)果籽前,所以我們要追加一個測試用例來規(guī)定這個屬性必須是UITableView類型的亭珍。
【tc 2.2.2,測試屬性theTableView是否是UITableView類型】
/**
tc 2.2.2
*/
- (void)test_Property_TheTableView_ShouldBeUITableViewType{
NSString *typeName = [self typeForProperty:@"theTableView" inClass:@"MyViewController"];
XCTAssertTrue([typeName isEqualToString:@"UITableView"]);
}
/**
用法:
1枝哄,如果是block類型的屬性肄梨,這個方法不能識別block的完整sinature,只能告知它是一個block挠锥,名字是什么众羡。
返回的字符串樣式是:"Block:[屬性名]"。
2蓖租,如果是id<協(xié)議1粱侣,協(xié)議2>類型,返回字符串樣式是:“<[協(xié)議1]><[協(xié)議2]>”蓖宦。
3齐婴,如果是普通對象屬性,返回字符串樣式是:“[類名]”稠茂。
@param pName <#pName description#>
@param cName <#cName description#>
@return <#return value description#>
*/
+ (NSString *)typeForProperty:(NSString *)pName inClass:(NSString *)cName{
unsigned int count;
Class checkClass = NSClassFromString(cName);
objc_property_t* props = class_copyPropertyList(checkClass, &count);
for (int i = 0; i < count; i++) {
objc_property_t property = props[i];
const char * name = property_getName(property);
NSString *propertyName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
if (![propertyName isEqualToString:pName]) {
continue;
}
const char * type = property_getAttributes(property);
//NSString *attr = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];
NSString * typeString = [NSString stringWithUTF8String:type];
NSArray * attributes = [typeString componentsSeparatedByString:@","];
NSString * typeAttribute = [attributes objectAtIndex:0];
NSString * propertyType = [typeAttribute substringFromIndex:1];
const char * rawPropertyType = [propertyType UTF8String];
if (strcmp(rawPropertyType, @encode(float)) == 0) {
//it's a float
} else if (strcmp(rawPropertyType, @encode(int)) == 0) {
//it's an int
} else if (strcmp(rawPropertyType, @encode(id)) == 0) {
//it's some sort of object
} else {
// According to Apples Documentation you can determine the corresponding encoding values
}
// 針對block屬性
if ([attributes containsObject:@"T@?"] &&([attributes containsObject:[NSString stringWithFormat:@"V_%@",propertyName]] || [attributes containsObject:[NSString stringWithFormat:@"V%@",propertyName]])) {
return [NSString stringWithFormat:@"Block:%@",propertyName];
}
if ([typeAttribute hasPrefix:@"T@"] && [typeAttribute length] > 1) {
NSString * typeClassName = [typeAttribute substringWithRange:NSMakeRange(3, [typeAttribute length]-4)]; //turns @"NSDate" into NSDate
Class typeClass = NSClassFromString(typeClassName);
if (typeClass != nil) {
// Here is the corresponding class even for nil values
}
return typeClassName;
}
}
free(props);
return nil;
}
【tc 2.2.2】使用到了OC的runtime來獲取屬性的類型柠偶,runtime在測試中是很常用的技術,可以說沒有runtime的支持睬关,很多東西都不能或不好去測試诱担。
新加進來的測試用例沒通過,這就是我們想要的結(jié)果电爹,只有當【tc 2.2.1蔫仙,tc 2.2.2】都通過時,屬性的設置才算是正確的丐箩。
現(xiàn)在摇邦,我們修改產(chǎn)品代碼恤煞,讓兩個測試用例都能通過:
#import <UIKit/UIKit.h>
@interface MyViewController : UIViewController
@property (nonatomic, strong) UITableView *theTableView;
@end
用同樣的方法,測試寫測試用例涎嚼,然后為控制器添加另一個theDataSource屬性阱州。注意測試屬性theDataSource類型的寫法跟測試屬性theTableView的不太一樣。
【tc 2.2.3法梯,測試是否存在theDataSource屬性】
【tc 2.2.4,測試theDataSource屬性是否是否遵循表格視圖的數(shù)據(jù)源和代理協(xié)議】
/**
tc 2.2.3
*/
- (void)test_PropertyExist_TheDataSource{
objc_property_t theTableViewProperty = class_getProperty([MyViewController class], "theDataSource");
XCTAssertTrue(theTableViewProperty != NULL);
}
/**
tc 2.2.4
*/
- (void)test_Property_TheDataSource_ShouldConformUITableViewDataSourceAndUITableViewDelegate{
NSString *typeName = [self typeForProperty:@"theDataSource" inClass:@"MyViewController"];
XCTAssertTrue([typeName isEqualToString:@"<UITableViewDataSource><UITableViewDelegate>"]);
}
滿足四個測試用例的產(chǎn)品代碼:
#import <UIKit/UIKit.h>
@interface MyViewController : UIViewController
@property (nonatomic, strong) UITableView *theTableView;
@property (nonatomic, strong) id<UITableViewDataSource,UITableViewDelegate> theDataSource;
@end
接下來犀概,我們測試(2)部分的協(xié)作立哑,在控制器存在表格視圖和數(shù)據(jù)源代理類的情況下,數(shù)據(jù)源代理類要作為表格視圖的數(shù)據(jù)源和代理姻灶,當控制器的viewDidLoad方法執(zhí)行之后我們就要確保這一點铛绰。
【tc 2.2.5,測試viewDidLoad之后产喉,控制器是否為表格視圖賦值了數(shù)據(jù)源】
【tc 2.2.6捂掰,測試viewDidLoad之后蚁飒,控制器是否為表格視圖賦值了代理】
/**
tc 2.2.5
*/
- (void)test_viewDidLoad_ConnetDataSourceToTableView{
MyViewController *vc = [[MyViewController alloc] init];
vc.theTableView = [[UITableView alloc] init];
vc.theDataSource = [[MyTableViewDataSource alloc] init];
[vc viewDidLoad];
XCTAssertTrue(vc.theTableView.dataSource == vc.theDataSource);
}
/**
tc 2.2.6
*/
- (void)test_viewDidLoad_ConnetDelegateToTableView{
MyViewController *vc = [[MyViewController alloc] init];
vc.theTableView = [[UITableView alloc] init];
vc.theDataSource = [[MyTableViewDataSource alloc] init];
[vc viewDidLoad];
XCTAssertTrue(vc.theTableView.delegate == vc.theDataSource);
}
實現(xiàn)控制器的viewDidLoad方法袖瞻,讓以上兩個測試用例通過。
- (void)viewDidLoad {
[super viewDidLoad];
self.theTableView.dataSource = self.theDataSource;
self.theTableView.delegate = self.theDataSource;
}
最后瑟蜈,我們測試(3)部分的協(xié)作塞俱,測試表格視圖是否接收到了數(shù)據(jù)源提供的正確數(shù)據(jù)姐帚。
待續(xù)。障涯。罐旗。。
demo:
https://github.com/zard0/TDDListModuleDemo.git