【iOS開(kāi)發(fā)】了解測(cè)試驅(qū)動(dòng)開(kāi)發(fā) (TDD)

什么是 TDD

測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Test-driven development, 簡(jiǎn)稱 TDD),是一種通過(guò)迭代進(jìn)行許多由測(cè)試支持的小更改的迭代開(kāi)發(fā)軟件的方法闻丑。

它有四個(gè)步驟:

  1. 寫(xiě)一個(gè)失敗的測(cè)試
  2. 使測(cè)試通過(guò)
  3. 重構(gòu)
  4. 重復(fù)

這個(gè)步驟也被稱為 TDD 循環(huán)漩怎,能徹底和準(zhǔn)確地測(cè)試代碼。

為什么應(yīng)該使用 TDD嗦嗡?

TDD 是確保軟件能夠正常工作并在未來(lái)繼續(xù)良好工作的唯一最佳方法勋锤。為什么?

你可以不按照 TDD 的方式來(lái)寫(xiě)測(cè)試代碼侥祭。例如叁执,先編寫(xiě)所有代碼,然后在寫(xiě)測(cè)試矮冬;或者完全跳過(guò)編寫(xiě)測(cè)試代碼谈宛,直接用手動(dòng)測(cè)試。為什么 TDD 比這些方法更好胎署?

因?yàn)?TDD 提供了確保測(cè)試良好的方法:

  • 第一步是編寫(xiě)一個(gè)失敗的測(cè)試吆录。根據(jù)定義,這證明了測(cè)試是可能失敗的琼牧。

  • 在編寫(xiě)新的測(cè)試之前恢筝,所有其他以前的測(cè)試都必須通過(guò)哀卫。這確保了測(cè)試的可重復(fù)性:不只是運(yùn)行正在進(jìn)行的單個(gè)測(cè)試,而是不斷地運(yùn)行所有測(cè)試撬槽。

  • 通過(guò)頻繁地運(yùn)行每個(gè)測(cè)試此改,您會(huì)受到激勵(lì),以確保測(cè)試能夠快速運(yùn)行侄柔。所有的測(cè)試僅需要幾秒鐘才能運(yùn)行共啃,最好是一秒鐘或更短。

  • 重構(gòu)時(shí)暂题,同時(shí)更新代碼和測(cè)試代碼移剪。這可以確保測(cè)試得到維護(hù)。

  • 通過(guò)并行迭代編寫(xiě)代碼和測(cè)試敢靡,可以確保代碼是可測(cè)試的挂滓。如果在完成代碼后編寫(xiě)測(cè)試,那么代碼很可能需要相當(dāng)多的重構(gòu)才能完成單元測(cè)試啸胧。

哪些是需要測(cè)試的入篮?

更好的測(cè)試覆蓋并不總是意味著你的應(yīng)用程序得到了更好的測(cè)試兜畸。有些事情你應(yīng)該測(cè)試析珊,有些事情你不應(yīng)該測(cè)試敢会。以下是注意事項(xiàng):

  • 為無(wú)法以自動(dòng)化方式捕獲的代碼編寫(xiě)測(cè)試。這包括類的方法中的代碼陷谱、自定義的 getter 和 setter 以及您自己編寫(xiě)的大多數(shù)其他內(nèi)容烙博。

  • 不要為自動(dòng)生成的代碼編寫(xiě)測(cè)試。例如烟逊,不值得為生成的 getter 和 setter 編寫(xiě)測(cè)試渣窜。

  • 不要為編譯器可能捕捉到的問(wèn)題編寫(xiě)測(cè)試。如果測(cè)試的問(wèn)題將生成錯(cuò)誤或警告宪躯,Xcode 將為您捕獲它乔宿。

  • 不要為依賴代碼編寫(xiě)測(cè)試,例如應(yīng)用程序使用的系統(tǒng)框架或第三方框架访雪∠耆穑框架作者負(fù)責(zé)編寫(xiě)這些測(cè)試。例如臣缀,不應(yīng)該為 UIKit 類編寫(xiě)測(cè)試坝橡,因?yàn)?UIKit 開(kāi)發(fā)人員負(fù)責(zé)編寫(xiě)這些測(cè)試。但是精置,應(yīng)該為自定義子類編寫(xiě)測(cè)試:這是自定義代碼计寇,因此你要負(fù)責(zé)編寫(xiě)測(cè)試。

上面的一個(gè)例外是編寫(xiě)測(cè)試以確定框架如何工作。這是非常有用的番宁。但是蹲堂,不需要長(zhǎng)期保存這些測(cè)試。相反贝淤,后續(xù)應(yīng)該刪除它們。

另一個(gè)例外是“健全性測(cè)試”政供,它可以確保第三方代碼如您所期望的那樣工作播聪。如果庫(kù)不是完全穩(wěn)定的,或者您不信任它布隔,那么這類測(cè)試非常有用离陶。

TDD 需要花太多時(shí)間

關(guān)于 TDD 最常見(jiàn)的抱怨是它花費(fèi)的時(shí)間太長(zhǎng)了。

但是衅檀,一旦你習(xí)慣了招刨,TDD 會(huì)變得更快。然而哀军,事實(shí)是沉眶,與根本不編寫(xiě)任何測(cè)試相比,您最終編寫(xiě)的代碼更多杉适。剛剛開(kāi)始用 TDD 可能需要更多的時(shí)間谎倔。

但是,你要知道:開(kāi)發(fā)的成本不僅僅是最開(kāi)始編寫(xiě)的第一個(gè)版本的代碼猿推。它還包括隨著時(shí)間的推移添加新功能片习、修改現(xiàn)有代碼、修復(fù)錯(cuò)誤等等蹬叭。從長(zhǎng)遠(yuǎn)來(lái)看藕咏,遵循 TDD 比不遵循 TDD 花費(fèi)的時(shí)間要少得多,因?yàn)樗拇a更易于維護(hù)秽五,錯(cuò)誤更少孽查。

還有另一個(gè)要考慮的成本:生產(chǎn)中缺陷對(duì)客戶的影響。一個(gè)問(wèn)題被發(fā)現(xiàn)的時(shí)間越長(zhǎng)筝蚕,成本就越高卦碾。它可能導(dǎo)致負(fù)面評(píng)論、失去信任和收入損失起宽。

如果在開(kāi)發(fā)過(guò)程中發(fā)現(xiàn)了問(wèn)題洲胖,那么調(diào)試起來(lái)更容易,修復(fù)也更快坯沪。如果你在幾周后發(fā)現(xiàn)它绿映,你將花費(fèi)更多的時(shí)間來(lái)加速代碼的運(yùn)行并找出根本原因。通過(guò)遵循 TDD,你的測(cè)試最終有助于保護(hù)你的應(yīng)用程序免受 bug 的影響叉弦。

什么時(shí)候應(yīng)該使用 TDD丐一?

TDD 可以在產(chǎn)品生命周期的任何時(shí)候使用:新開(kāi)發(fā)的、已存在的應(yīng)用程序以及介于兩者之間的一切淹冰。然而库车,如何以及從哪里開(kāi)始 TDD 確實(shí)取決于項(xiàng)目的狀態(tài)。

然而樱拴,有一個(gè)重要的問(wèn)題需要問(wèn):您的項(xiàng)目是否應(yīng)該使用 TDD柠衍?

一般來(lái)說(shuō),如果你的應(yīng)用要持續(xù)幾個(gè)月以上晶乔,會(huì)有多個(gè)版本和/或需要復(fù)雜的邏輯珍坊,那么你最好還是使用 TDD。

如果你為一些臨時(shí)性的東西創(chuàng)建一個(gè)應(yīng)用程序正罢,你應(yīng)該評(píng)估 TDD 是否有意義阵漏。如果真的只有一個(gè)版本的應(yīng)用程序,你可能不會(huì)遵循 TDD翻具,或者只對(duì)關(guān)鍵或困難的部分進(jìn)行 TDD履怯。

歸根結(jié)底,TDD是一種工具呛占,您可以決定何時(shí)最好地使用它虑乖!

TDD 簡(jiǎn)單案例

在上面的內(nèi)容已經(jīng)提到,TDD 的流程有四個(gè)步驟:

  1. 寫(xiě)一個(gè)失敗的測(cè)試
  2. 使測(cè)試通過(guò)
  3. 重構(gòu)
  4. 重復(fù)

下面通過(guò)例子來(lái)演示每個(gè)步驟晾虑。

寫(xiě)一個(gè)失敗的測(cè)試

在 playground 中編寫(xiě)以下測(cè)試用例:

class CashRegisterTests: XCTestCase {
    func testInit_createsCashRegister() {
        XCTAssertNotNil(CashRegister())
    }
}

// 調(diào)用這個(gè)可以在 playground 運(yùn)行測(cè)試
CashRegisterTests.defaultTestSuite.run()

在 iOS 中疹味,測(cè)試用例的方法都是以 test 開(kāi)頭。因?yàn)闆](méi)有定義 CashRegister帜篇,所以編譯器報(bào)錯(cuò)糙捺。而對(duì)于 TDD 循環(huán)來(lái)說(shuō),編譯報(bào)錯(cuò)可以看做是測(cè)試失敗笙隙,所以到此我們完成第一步:寫(xiě)一個(gè)失敗的測(cè)試洪灯。

使測(cè)試通過(guò)

如果編寫(xiě)最少的代碼使得上面的測(cè)試通過(guò)?當(dāng)然是定義 CashRegister竟痰。

class CashRegister {

}

執(zhí)行 playground 后签钩,得到以下輸出:

Test Suite 'CashRegisterTests' started at 2020-08-06 02:17:19.397
Test Case '-[__lldb_expr_1.CashRegisterTests testInit_createsCashRegister]' started.
Test Case '-[__lldb_expr_1.CashRegisterTests testInit_createsCashRegister]' passed (0.210 seconds).
Test Suite 'CashRegisterTests' passed at 2020-08-06 02:17:19.608.
     Executed 1 test, with 0 failures (0 unexpected) in 0.210 (0.212) seconds

可以看到測(cè)試通過(guò),所以到此我們完成第二步:使測(cè)試通過(guò)坏快。

重構(gòu)

在這一步中铅檩,我們將清理應(yīng)用程序代碼和測(cè)試代碼。通過(guò)這樣做莽鸿,可以不斷地維護(hù)和改進(jìn)代碼昧旨。以下是一些可以重構(gòu)的內(nèi)容:

  • 重復(fù)的邏輯:反問(wèn)自己拾给,能抽出任何屬性、方法或類來(lái)消除重復(fù)嗎兔沃?

  • 注釋:注釋?xiě)?yīng)該解釋為什么要做某件事蒋得,而不是怎么做的。盡量消除解釋代碼工作原理的注釋乒疏。應(yīng)該通過(guò)將復(fù)雜方法分解為幾個(gè)命名良好的方法额衙,重命名屬性和方法以使其更清晰,或者有時(shí)只是簡(jiǎn)單地將代碼結(jié)構(gòu)更好地表達(dá)出來(lái)怕吴。

  • 錯(cuò)誤代碼:有時(shí)某個(gè)特定的代碼塊似乎是錯(cuò)誤的入偷。相信你的直覺(jué),試著消除這些錯(cuò)誤代碼械哟。例如,你可能有做過(guò)多假設(shè)的邏輯殿雪,使用硬編碼字符串或有其他問(wèn)題暇咆。

現(xiàn)在, CashRegisterCashRegisterTests 沒(méi)有太多的邏輯丙曙,也沒(méi)有什么可以重構(gòu)的爸业。所以到此我們完成第三步:重構(gòu)。

重復(fù)

前面已經(jīng)完成了第一個(gè) TDD 周期亏镰,現(xiàn)在我們重復(fù)這個(gè)周期扯旷。在這內(nèi)容,將為 CashRegister 添加以下方法:

  • 接受可用資金參數(shù)的初始化器
  • 用于添加 item 的方法

接受可用資金參數(shù)的初始化函數(shù)

首先寫(xiě)測(cè)試用例:

func testInitAvailableFunds_setsAvailableFunds() {
    // given
    let availableFunds: Decimal = 100
    
    // when
    let sut = CashRegister(availableFunds: availableFunds)
    
    // then
    XCTAssertEqual(sut.availableFunds, availableFunds)
}

編譯錯(cuò)誤索抓,因?yàn)?CashRegister 還沒(méi)有那個(gè)初始化函數(shù)钧忽。下面編寫(xiě)這個(gè)函數(shù):

class CashRegister {
    var availableFunds: Decimal
    
    init(availableFunds: Decimal = 0) {
        self.availableFunds = availableFunds
    }
}

編譯錯(cuò)誤消失,執(zhí)行 playground 后逼肯,得到以下輸出:

Test Suite 'CashRegisterTests' started at 2020-08-06 09:42:40.794
Test Case '-[__lldb_expr_3.CashRegisterTests testInit_createsCashRegister]' started.
Test Case '-[__lldb_expr_3.CashRegisterTests testInit_createsCashRegister]' passed (0.174 seconds).
Test Case '-[__lldb_expr_3.CashRegisterTests testInitAvailableFunds_setsAvailableFunds]' started.
Test Case '-[__lldb_expr_3.CashRegisterTests testInitAvailableFunds_setsAvailableFunds]' passed (0.008 seconds).
Test Suite 'CashRegisterTests' passed at 2020-08-06 09:42:40.978.
     Executed 2 tests, with 0 failures (0 unexpected) in 0.182 (0.184) seconds

可以看到測(cè)試通過(guò)耸黑。到此我們完成了兩個(gè)步驟:1. 寫(xiě)一個(gè)失敗的測(cè)試;2. 使測(cè)試通過(guò)篮幢。

第三步是重構(gòu)大刊,這一步中我們將清理應(yīng)用程序代碼和測(cè)試代碼。

對(duì)于測(cè)試代碼三椿,可以發(fā)現(xiàn) testInit_createsCashRegister 是沒(méi)必要的缺菌,所以可以把它刪掉。

對(duì)于應(yīng)用代碼搜锰,初始化器 init(availableFunds: Decimal = 0) 的參數(shù)有一個(gè)默認(rèn)值伴郁,我們得思考這個(gè)默認(rèn)值是否有必要?這將會(huì)產(chǎn)生以下兩種情況:

  • 如果保留默認(rèn)值纽乱,那么就得考慮為這個(gè)默認(rèn)值添加測(cè)試
  • 如果不保留默認(rèn)值蛾绎,那么就把它刪掉

在這里,我們把它刪掉,變成:

init(availableFunds: Decimal) {
    self.availableFunds = availableFunds
}

重構(gòu)完成后租冠,繼續(xù)執(zhí)行 playground鹏倘,發(fā)現(xiàn)還能測(cè)試通過(guò)。通過(guò)這個(gè)例子顽爹,可以感覺(jué)到遵循 TDD 能讓我們?cè)谥貥?gòu)時(shí)更有信心纤泵。

用于添加 item 的方法

首先寫(xiě)測(cè)試用例:

func testAddItem_oneItem_addsCostToTransactionTotal() {
    // given
    let availableFunds: Decimal = 100
    let sut = CashRegister(availableFunds: availableFunds)
    let itemCost: Decimal = 42
    
    // when
    sut.addItem(itemCost)
    
    // then
    XCTAssertEqual(sut.transactionTotal, itemCost)
}

編譯錯(cuò)誤,因?yàn)?CashRegister 還沒(méi)有 addItem 方法和 transactionTotal 屬性镜粤。下面編寫(xiě) addItem 方法和 transactionTotal 屬性:

class CashRegister {
    var transactionTotal: Decimal = 0
    var availableFunds: Decimal
    
    init(availableFunds: Decimal = 0) {
        self.availableFunds = availableFunds
    }
    
    func addItem(_ cost: Decimal) {
        transactionTotal = cost
    }
}

addItem 方法的實(shí)現(xiàn)中捏题,直接把 cost 賦值給 transactionTotal 很明顯是不對(duì)的。但對(duì)于這個(gè)測(cè)試用例來(lái)說(shuō)肉渴,這么做也能讓編譯錯(cuò)誤消失公荧。所以我們暫時(shí)先這么做。執(zhí)行 playground 后同规,可以看到測(cè)試通過(guò)循狰。

接下來(lái)進(jìn)行重構(gòu)。

對(duì)于測(cè)試代碼券勺,可以發(fā)現(xiàn)一下的代碼在測(cè)試用例中重復(fù)了:

let availableFunds: Decimal = 100
let sut = CashRegister(availableFunds: availableFunds)

所以我們可以把這兩個(gè)變量抽出來(lái)绪钥,作為 CashRegisterTests 的屬性,并且在 setUp()tearDown() 方法中初始化和重置他們的值关炼。最終重構(gòu)后的代碼如下:

class CashRegisterTests: XCTestCase {
    
    var availableFunds: Decimal!
    var sut: CashRegister!
    
    override func setUp() {
        super.setUp()
        availableFunds = 100
        sut = CashRegister(availableFunds: availableFunds)
    }

    override func tearDown() {
        availableFunds = nil
        sut = nil
        super.tearDown()
    }
    
    func testInitAvailableFunds_setsAvailableFunds() {
        XCTAssertEqual(sut.availableFunds, availableFunds)
    }
    
    func testAddItem_oneItem_addsCostToTransactionTotal() {
        // given
        let itemCost: Decimal = 42
        
        // when
        sut.addItem(itemCost)
        
        // then
        XCTAssertEqual(sut.transactionTotal, itemCost)
    }
}

setUp() 中初始哈變量的值程腹;在 tearDown() 方法中重置變量的值。另外儒拂,我們應(yīng)該總是在 tearDown() 中把變量設(shè)置為 nil寸潦,因?yàn)?XCTestCase 類只在所有測(cè)試完成之后才會(huì)釋放變量占用的內(nèi)存,所以如果我們有很多測(cè)試用例并且不在 tearDown() 中把變量設(shè)置為 nil的話社痛,那么測(cè)試的性能可能會(huì)受到影響甸祭。

添加兩個(gè) items

testAddItem_oneItem 測(cè)試用例證明了 addItem() 在添加一個(gè) item 時(shí)是正確的。如果添加兩個(gè)或更多 items 時(shí)會(huì)怎樣呢褥影?我們來(lái)測(cè)試一下池户。

添加測(cè)試用例如下:

func testAddItem_twoItems_addsCostsToTransactionTotal() {
    // given
    let itemCost: Decimal = 42
    let itemCost2: Decimal = 20
    let expectedTotal = itemCost + itemCost2
    
    // when
    sut.addItem(itemCost)
    sut.addItem(itemCost2)
    
    // then
    XCTAssertEqual(sut.transactionTotal, expectedTotal)
}

執(zhí)行后,發(fā)現(xiàn)剛剛添加的測(cè)試用例失敗了凡怎。這證明 addItem() 方法的實(shí)現(xiàn)有問(wèn)題校焦,我們很快找到問(wèn)題所在,把實(shí)現(xiàn)改為:

transactionTotal += cost

再次執(zhí)行后统倒,所有測(cè)試通過(guò)寨典。

接下來(lái)是重構(gòu):仔細(xì)查看測(cè)試代碼,發(fā)現(xiàn) itemCost 可以抽取出來(lái)房匆,重構(gòu)后代碼為:

class CashRegisterTests: XCTestCase {
    
    var availableFunds: Decimal!
    var sut: CashRegister!
    var itemCost: Decimal!
    
    override func setUp() {
        super.setUp()
        availableFunds = 100
        sut = CashRegister(availableFunds: availableFunds)
        itemCost = 42
    }

    override func tearDown() {
        availableFunds = nil
        sut = nil
        itemCost = nil
        super.tearDown()
    }
    
    func testInitAvailableFunds_setsAvailableFunds() {
        XCTAssertEqual(sut.availableFunds, availableFunds)
    }
    
    func testAddItem_oneItem_addsCostToTransactionTotal() {
        // when
        sut.addItem(itemCost)
        
        // then
        XCTAssertEqual(sut.transactionTotal, itemCost)
    }
    
    func testAddItem_twoItems_addsCostsToTransactionTotal() {
        // given
        let itemCost2: Decimal = 20
        let expectedTotal = itemCost + itemCost2
        
        // when
        sut.addItem(itemCost)
        sut.addItem(itemCost2)
        
        // then
        XCTAssertEqual(sut.transactionTotal, expectedTotal)
    }
}

參考資料

iOS Test-Driven Development by Tutorials

有問(wèn)題可以直接留言耸成。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末报亩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子井氢,更是在濱河造成了極大的恐慌弦追,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件花竞,死亡現(xiàn)場(chǎng)離奇詭異劲件,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)约急,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)零远,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人厌蔽,你說(shuō)我怎么就攤上這事牵辣。” “怎么了奴饮?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵服猪,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拐云,道長(zhǎng),這世上最難降的妖魔是什么近她? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任叉瘩,我火速辦了婚禮,結(jié)果婚禮上粘捎,老公的妹妹穿的比我還像新娘薇缅。我一直安慰自己,他們只是感情好攒磨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布泳桦。 她就那樣靜靜地躺著,像睡著了一般娩缰。 火紅的嫁衣襯著肌膚如雪灸撰。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天拼坎,我揣著相機(jī)與錄音浮毯,去河邊找鬼。 笑死泰鸡,一個(gè)胖子當(dāng)著我的面吹牛债蓝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播盛龄,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼饰迹,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼芳誓!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起啊鸭,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤锹淌,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后莉掂,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體葛圃,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年憎妙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了库正。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡厘唾,死狀恐怖褥符,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情抚垃,我是刑警寧澤喷楣,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站鹤树,受9級(jí)特大地震影響铣焊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜罕伯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一曲伊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧追他,春花似錦坟募、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至单雾,卻和暖如春赚哗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背硅堆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工蜂奸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人硬萍。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓扩所,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親朴乖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子祖屏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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