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)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,205評論 2 375

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