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

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

image.png

雖然前面【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)了控制器的測試用例里面有一個失敗了潮改。


image.png

反正我們也要開始改控制器的代碼了,就把這個測試用例作為我們這次對控制器修改的第一個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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末叠穆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子臼膏,更是在濱河造成了極大的恐慌硼被,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渗磅,死亡現(xiàn)場離奇詭異嚷硫,居然都是意外死亡,警方通過查閱死者的電腦和手機始鱼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門论巍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人风响,你說我怎么就攤上這事嘉汰。” “怎么了状勤?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵鞋怀,是天一觀的道長双泪。 經(jīng)常有香客問我,道長密似,這世上最難降的妖魔是什么焙矛? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮残腌,結(jié)果婚禮上村斟,老公的妹妹穿的比我還像新娘。我一直安慰自己抛猫,他們只是感情好蟆盹,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著闺金,像睡著了一般逾滥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上败匹,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天寨昙,我揣著相機與錄音,去河邊找鬼掀亩。 笑死舔哪,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的槽棍。 我是一名探鬼主播捉蚤,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼刹泄!你這毒婦竟也來了外里?” 一聲冷哼從身側(cè)響起怎爵,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤特石,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鳖链,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體姆蘸,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年芙委,在試婚紗的時候發(fā)現(xiàn)自己被綠了逞敷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡灌侣,死狀恐怖推捐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情侧啼,我是刑警寧澤牛柒,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布堪簿,位于F島的核電站,受9級特大地震影響皮壁,放射性物質(zhì)發(fā)生泄漏椭更。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一蛾魄、第九天 我趴在偏房一處隱蔽的房頂上張望虑瀑。 院中可真熱鬧,春花似錦滴须、人聲如沸舌狗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锋华,卻和暖如春铭污,著一層夾襖步出監(jiān)牢的瞬間恋日,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工嘹狞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留岂膳,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓磅网,卻偏偏與公主長得像谈截,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涧偷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

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