構(gòu)筑測試體系

重構(gòu)是很有價值的工具月洛,但只有重構(gòu)還不行。要正確地進(jìn)行重構(gòu)捌议,前提是得有一套穩(wěn)固的測試集合哼拔,以幫我發(fā)現(xiàn)難以避免的疏漏。編寫優(yōu)良的測試程序瓣颅,可以極大提高我的編程速度倦逐,也許這會違反許多程序員的直覺。

1 自測試代碼的價值

如果你認(rèn)真觀察自己如何分配時間宫补,就會發(fā)現(xiàn)檬姥,編寫代碼的時間僅占有所有時間很少的一部分,有些時間花在設(shè)計(jì)上粉怕,有些時間用來決定下一步干什么健民,但是花費(fèi)在調(diào)試的時間是最多的。修復(fù)bug通常是比較快的贫贝,但找出bug所在卻是一場噩夢秉犹。
我需要做的就是把我所期望的輸出放到測試代碼中脸侥,然后做一個對比就行了讨越。由于我頻繁地運(yùn)行測試,每次測試都在不久之前爵憎,因此我知道bug地源頭就是我剛剛寫下的代碼客燕。因?yàn)榇a量很少鸳劳,我對它也記憶尤新,所以就能輕松找出bug也搓。
注意到這一點(diǎn)后赏廓,我對測試的積極性提高了涵紊。我不再等待每次迭代結(jié)尾時再增加測試,而是只要寫好一個功能點(diǎn)幔摸,就立即添加她們栖袋。
說服別人也這么做并不容易,編寫測試程序抚太,意味著要寫很多額外的代碼。除非你確實(shí)體會到這種方法是如何提升編程速度的昔案,否則自測試似乎就沒什么意義尿贫。
事實(shí)上,撰寫測試代碼的最好時機(jī)是在開始動手編碼之前踏揣。編寫測試代碼其實(shí)就是在問自己:為了添加這個功能庆亡,我需要實(shí)現(xiàn)些什么,編寫測試代碼還能幫我把注意力集中于接口而非實(shí)現(xiàn)捞稿。
先編寫一個測試又谋,編寫代碼使測試通過,然后進(jìn)行重構(gòu)保證代碼整潔娱局。這個“測試彰亥、編碼、重構(gòu)”的循環(huán)應(yīng)該在每個小時內(nèi)都完成很多次衰齐。這種良好的節(jié)奏感可使編程工作更加高效任斋、有條不紊的方式開展。
有時我需要重構(gòu)一些沒有測試的代碼耻涛,在重構(gòu)之前废酷,我得先改造這些代碼,使其能夠自測試才行抹缕。

2 如何編寫單元測試

寫單元測試就是針對代碼設(shè)計(jì)各種測試用例澈蟆,以覆蓋各種輸入、異常卓研、邊界情況趴俘,并將其翻譯成代碼。我們可以利用一些測試框架來簡化單元測試的編寫鉴分。除此之外哮幢,對于單元測試,我們需要建立以下正確的認(rèn)知:

  • 編寫單元測試盡管繁瑣志珍,但并不是太耗時橙垢;
  • 我們可以稍微放低對單元測試代碼質(zhì)量的要求;
  • 覆蓋率作為衡量單元測試質(zhì)量的唯一標(biāo)準(zhǔn)是不合理的伦糯,實(shí)際上柜某,更重要的是要看測試用例是否覆蓋了所有可能的情況嗽元,特別是一些 corner case;
  • 單元測試不要依賴被測代碼的具體實(shí)現(xiàn)邏輯喂击,只需要關(guān)注代碼實(shí)現(xiàn)的功能即可剂癌;
  • 單元測試框架無法測試,多半是因?yàn)榇a的可測試性不好翰绊。

3 單元測試為何難以落地

很多人往往會覺得寫單元測試比較繁瑣佩谷,并且沒有太多挑戰(zhàn),而不愿意去做监嗜。有很多團(tuán)隊(duì)和項(xiàng)目在剛開始推行單元測試的時候谐檀,還比較認(rèn)真,執(zhí)行得比較好裁奇。但當(dāng)開發(fā)任務(wù)緊了之后桐猬,就開始放低對單元測試的要求,一旦出現(xiàn)破窗效應(yīng)刽肠,慢慢的溃肪,大家就都不寫了,這種情況很常見音五。
由于歷史遺留問題惫撰,原來的代碼都沒有寫單元測試,代碼已經(jīng)堆砌了十幾萬行了躺涝,不可能再一個一個去補(bǔ)單元測試润绎。這種情況下,我們首先要保證新寫的代碼都要有單元測試诞挨,其次莉撇,每次在改動到某個類時,如果沒有單元測試就順便補(bǔ)上惶傻,不過這要求工程師們有足夠強(qiáng)的主人翁意識(ownership)棍郎,畢竟光靠 leader 督促,很多事情是很難執(zhí)行到位的银室。
寫好代碼直接提交涂佃,然后丟給黑盒測試狠命去測,測出問題就反饋給開發(fā)團(tuán)隊(duì)再修改蜈敢,測不出的問題就留在線上出了問題再修復(fù)辜荠。在這樣的開發(fā)模式下,團(tuán)隊(duì)往往覺得沒有必要寫單元測試抓狭,但如果我們把單元測試寫好伯病、做好 Code Review,重視起代碼質(zhì)量否过,其實(shí)可以很大程度上減少黑盒測試的投入午笛。

4 案例實(shí)戰(zhàn)

實(shí)戰(zhàn)內(nèi)容來自《極客時間》專欄《設(shè)計(jì)模式之美》惭蟋,有興趣的可以看下原文。
Transaction 是經(jīng)過我抽象簡化之后的一個電商系統(tǒng)的交易類药磺,用來記錄每筆訂單交易的情況告组。Transaction 類中的 execute() 函數(shù)負(fù)責(zé)執(zhí)行轉(zhuǎn)賬操作,將錢從買家的錢包轉(zhuǎn)到賣家的錢包中癌佩。真正的轉(zhuǎn)賬操作是通過調(diào)用 WalletRpcService RPC 服務(wù)來完成的木缝。除此之外,代碼中還涉及一個分布式鎖 DistributedLock 單例類围辙,用來避免 Transaction 并發(fā)執(zhí)行氨肌,導(dǎo)致用戶的錢被重復(fù)轉(zhuǎn)出。

public class Transaction {
  private String id;
  private Long buyerId;
  private Long sellerId;
  private Long productId;
  private String orderId;
  private Long createTimestamp;
  private Double amount;
  private STATUS status;
  private String walletTransactionId;
  
  // ...get() methods...
  
  public Transaction(String preAssignedId, Long buyerId, Long sellerId, Long productId, String orderId) {
    if (preAssignedId != null && !preAssignedId.isEmpty()) {
      this.id = preAssignedId;
    } else {
      this.id = IdGenerator.generateTransactionId();
    }
    if (!this.id.startWith("t_")) {
      this.id = "t_" + preAssignedId;
    }
    this.buyerId = buyerId;
    this.sellerId = sellerId;
    this.productId = productId;
    this.orderId = orderId;
    this.status = STATUS.TO_BE_EXECUTD;
    this.createTimestamp = System.currentTimestamp();
  }
  
  public boolean execute() throws InvalidTransactionException {
    if ((buyerId == null || (sellerId == null || amount < 0.0) {
      throw new InvalidTransactionException(...);
    }
    if (status == STATUS.EXECUTED) return true;
    boolean isLocked = false;
    try {
      isLocked = RedisDistributedLock.getSingletonIntance().lockTransction(id);
      if (!isLocked) {
        return false; // 鎖定未成功酌畜,返回false,job兜底執(zhí)行
      }
      if (status == STATUS.EXECUTED) return true; // double check
      long executionInvokedTimestamp = System.currentTimestamp();
      if (executionInvokedTimestamp - createdTimestap > 14days) {
        this.status = STATUS.EXPIRED;
        return false;
      }
      WalletRpcService walletRpcService = new WalletRpcService();
      String walletTransactionId = walletRpcService.moveMoney(id, buyerId, sellerId, amount);
      if (walletTransactionId != null) {
        this.walletTransactionId = walletTransactionId;
        this.status = STATUS.EXECUTED;
        return true;
      } else {
        this.status = STATUS.FAILED;
        return false;
      }
    } finally {
      if (isLocked) {
       RedisDistributedLock.getSingletonIntance().unlockTransction(id);
      }
    }
  }
}

在 Transaction 類中卿叽,主要邏輯集中在 execute() 函數(shù)中桥胞,所以它是我們測試的重點(diǎn)對象。為了盡可能全面覆蓋各種正常和異常情況考婴,針對這個函數(shù)贩虾,我設(shè)計(jì)了下面 6 個測試用例。

  1. 正常情況下沥阱,交易執(zhí)行成功缎罢,回填用于對賬(交易與錢包的交易流水)用的 walletTransactionId,交易狀態(tài)設(shè)置為 EXECUTED考杉,函數(shù)返回 true策精。
  2. buyerId、sellerId 為 null崇棠、amount 小于 0咽袜,返回 InvalidTransactionException。
  3. 交易已過期(createTimestamp 超過 14 天)枕稀,交易狀態(tài)設(shè)置為 EXPIRED询刹,返回 false。
  4. 交易已經(jīng)執(zhí)行了(status==EXECUTED)萎坷,不再重復(fù)執(zhí)行轉(zhuǎn)錢邏輯凹联,返回 true。
  5. 錢包(WalletRpcService)轉(zhuǎn)錢失敗哆档,交易狀態(tài)設(shè)置為 FAILED蔽挠,函數(shù)返回 false。
  6. 交易正在執(zhí)行著瓜浸,不會被重復(fù)執(zhí)行象泵,函數(shù)直接返回 false寞秃。
    對于上面的測試用例,第 2 個實(shí)現(xiàn)起來非常簡單偶惠,我就不做介紹了春寿。我們重點(diǎn)來看其中的 1 和 3。
    現(xiàn)在忽孽,我們就來看測試用例 1 的代碼實(shí)現(xiàn)绑改。
public void testExecute() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId);
  boolean executedResult = transaction.execute();
  assertTrue(executedResult);
}

execute() 函數(shù)的執(zhí)行依賴兩個外部的服務(wù),一個是 RedisDistributedLock兄一,一個 WalletRpcService厘线。這就導(dǎo)致上面的單元測試代碼存在下面幾個問題。

  • 如果要讓這個單元測試能夠運(yùn)行出革,我們需要搭建 Redis 服務(wù)和 Wallet RPC 服務(wù)造壮。搭建和維護(hù)的成本比較高。
  • 我們還需要保證將偽造的 transaction 數(shù)據(jù)發(fā)送給 Wallet RPC 服務(wù)之后骂束,能夠正確返回我們期望的結(jié)果耳璧,然而 Wallet RPC 服務(wù)有可能是第三方(另一個團(tuán)隊(duì)開發(fā)維護(hù)的)的服務(wù),并不是我們可控的展箱。換句話說旨枯,并不是我們想讓它返回什么數(shù)據(jù)就返回什么。
  • Transaction 的執(zhí)行跟 Redis混驰、RPC 服務(wù)通信攀隔,需要走網(wǎng)絡(luò),耗時可能會比較長栖榨,對單元測試本身的執(zhí)行性能也會有影響昆汹。
  • 網(wǎng)絡(luò)的中斷、超時婴栽、Redis筹煮、RPC 服務(wù)的不可用,都會影響單元測試的執(zhí)行居夹。
    如果代碼中依賴了外部系統(tǒng)或者不可控組件败潦,比如,需要依賴數(shù)據(jù)庫准脂、網(wǎng)絡(luò)通信劫扒、文件系統(tǒng)等,那我們就需要將被測代碼與外部系統(tǒng)解依賴狸膏,而這種解依賴的方法就叫作“mock”沟饥。所謂的 mock 就是用一個“假”的服務(wù)替換真正的服務(wù)。mock 的服務(wù)完全在我們的控制之下,模擬輸出我們想要的數(shù)據(jù)贤旷。
    我們通過繼承 WalletRpcService 類广料,并且重寫其中的 moveMoney() 函數(shù)的方式來實(shí)現(xiàn) mock。具體的代碼實(shí)現(xiàn)如下所示幼驶。通過 mock 的方式艾杏,我們可以讓 moveMoney() 返回任意我們想要的數(shù)據(jù),完全在我們的控制范圍內(nèi)盅藻,并且不需要真正進(jìn)行網(wǎng)絡(luò)通信购桑。
public class MockWalletRpcServiceOne extends WalletRpcService {
  public String moveMoney(Long id, Long fromUserId, Long toUserId, Double amount) {
    return "123bac";
  } 
}

public class MockWalletRpcServiceTwo extends WalletRpcService {
  public String moveMoney(Long id, Long fromUserId, Long toUserId, Double amount) {
    return null;
  } 
}

因?yàn)?WalletRpcService 是在 execute() 函數(shù)中通過 new 的方式創(chuàng)建的,我們無法動態(tài)地對其進(jìn)行替換氏淑。也就是說勃蜘,Transaction 類中的 execute() 方法的可測試性很差,需要通過重構(gòu)來讓其變得更容易測試假残。該如何重構(gòu)這段代碼呢缭贡?
我們可以應(yīng)用依賴注入,將 WalletRpcService 對象的創(chuàng)建反轉(zhuǎn)給上層邏輯辉懒,在外部創(chuàng)建好之后阳惹,再注入到 Transaction 類中。重構(gòu)之后的 Transaction 類的代碼如下所示:

public class Transaction {
  //...
  // 添加一個成員變量及其set方法
  private WalletRpcService walletRpcService;
  
  public void setWalletRpcService(WalletRpcService walletRpcService) {
    this.walletRpcService = walletRpcService;
  }
  // ...
  public boolean execute() {
    // ...
    // 刪除下面這一行代碼
    // WalletRpcService walletRpcService = new WalletRpcService();
    // ...
  }
}

現(xiàn)在耗帕,我們就可以在單元測試中,非常容易地將 WalletRpcService 替換成 MockWalletRpcServiceOne 或 WalletRpcServiceTwo 了袱贮。重構(gòu)之后的代碼對應(yīng)的單元測試如下所示:

public void testExecute() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId);
  // 使用mock對象來替代真正的RPC服務(wù)
  transaction.setWalletRpcService(new MockWalletRpcServiceOne()):
  boolean executedResult = transaction.execute();
  assertTrue(executedResult);
  assertEquals(STATUS.EXECUTED, transaction.getStatus());
}

WalletRpcService 的 mock 和替換問題解決了仿便,我們再來看 RedisDistributedLock。RedisDistributedLock 是一個單例類攒巍。單例相當(dāng)于一個全局變量嗽仪,我們無法 mock(無法繼承和重寫方法),也無法通過依賴注入的方式來替換柒莉。
如果 RedisDistributedLock 是我們自己維護(hù)的闻坚,可以自由修改、重構(gòu)兢孝,那我們可以將其改為非單例的模式窿凤,或者定義一個接口,比如 IDistributedLock跨蟹,讓 RedisDistributedLock 實(shí)現(xiàn)這個接口雳殊。這樣我們就可以像前面 WalletRpcService 的替換方式那樣,替換 RedisDistributedLock 為 MockRedisDistributedLock 了窗轩。但如果 RedisDistributedLock 不是我們維護(hù)的夯秃,我們無權(quán)去修改這部分代碼,這個時候該怎么辦呢?
我們可以對 transaction 上鎖這部分邏輯重新封裝一下仓洼。具體代碼實(shí)現(xiàn)如下所示:

public class TransactionLock {
  public boolean lock(String id) {
    return RedisDistributedLock.getSingletonIntance().lockTransction(id);
  }
  
  public void unlock() {
    RedisDistributedLock.getSingletonIntance().unlockTransction(id);
  }
}

public class Transaction {
  //...
  private TransactionLock lock;
  
  public void setTransactionLock(TransactionLock lock) {
    this.lock = lock;
  }
 
  public boolean execute() {
    //...
    try {
      isLocked = lock.lock();
      //...
    } finally {
      if (isLocked) {
        lock.unlock();
      }
    }
    //...
  }
}

針對重構(gòu)過的代碼介陶,我們的單元測試代碼修改為下面這個樣子。這樣色建,我們就能在單元測試代碼中隔離真正的 RedisDistributedLock 分布式鎖這部分邏輯了哺呜。

public void testExecute() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  
  TransactionLock mockLock = new TransactionLock() {
    public boolean lock(String id) {
      return true;
    }
  
    public void unlock() {}
  };
  
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId);
  transaction.setWalletRpcService(new MockWalletRpcServiceOne());
  transaction.setTransactionLock(mockLock);
  boolean executedResult = transaction.execute();
  assertTrue(executedResult);
  assertEquals(STATUS.EXECUTED, transaction.getStatus());
}

至此,測試用例 1 就算寫好了镀岛。我們通過依賴注入和 mock弦牡,讓單元測試代碼不依賴任何不可控的外部服務(wù)。你可以照著這個思路漂羊,自己寫一下測試用例 4驾锰、5、6走越。
現(xiàn)在椭豫,我們再來看測試用例 3:交易已過期(createTimestamp 超過 14 天),交易狀態(tài)設(shè)置為 EXPIRED旨指,返回 false赏酥。針對這個單元測試用例,我們還是先把代碼寫出來谆构,然后再來分析裸扶。

public void testExecute_with_TransactionIsExpired() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId);
  transaction.setCreatedTimestamp(System.currentTimestamp() - 14days);
  boolean actualResult = transaction.execute();
  assertFalse(actualResult);
  assertEquals(STATUS.EXPIRED, transaction.getStatus());
}

那如果沒有針對 createTimestamp 的 set 方法,那測試用例 3 又該如何實(shí)現(xiàn)呢搬素?實(shí)際上呵晨,這是一類比較常見的問題,就是代碼中包含跟“時間”有關(guān)的“未決行為”邏輯熬尺。我們一般的處理方式是將這種未決行為邏輯重新封裝摸屠。針對 Transaction 類,我們只需要將交易是否過期的邏輯粱哼,封裝到 isExpired() 函數(shù)中即可季二,具體的代碼實(shí)現(xiàn)如下所示:

public class Transaction {

  protected boolean isExpired() {
    long executionInvokedTimestamp = System.currentTimestamp();
    return executionInvokedTimestamp - createdTimestamp > 14days;
  }
  
  public boolean execute() throws InvalidTransactionException {
    //...
      if (isExpired()) {
        this.status = STATUS.EXPIRED;
        return false;
      }
    //...
  }
}

針對重構(gòu)之后的代碼,測試用例 3 的代碼實(shí)現(xiàn)如下所示:

public void testExecute_with_TransactionIsExpired() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId) {
    protected boolean isExpired() {
      return true;
    }
  };
  boolean actualResult = transaction.execute();
  assertFalse(actualResult);
  assertEquals(STATUS.EXPIRED, transaction.getStatus());
}

實(shí)際上揭措,可測試性差的代碼胯舷,本身代碼設(shè)計(jì)得也不夠好,很多地方都沒有遵守我們之前講到的設(shè)計(jì)原則和思想绊含,比如“基于接口而非實(shí)現(xiàn)編程”思想需纳、依賴反轉(zhuǎn)原則等。重構(gòu)之后的代碼艺挪,不僅可測試性更好不翩,而且從代碼設(shè)計(jì)的角度來說兵扬,也遵從了經(jīng)典的設(shè)計(jì)原則和思想。這也印證了我們之前說過的口蝠,代碼的可測試性可以從側(cè)面上反應(yīng)代碼設(shè)計(jì)是否合理器钟。除此之外,在平時的開發(fā)中妙蔗,我們也要多思考一下傲霸,這樣編寫代碼,是否容易編寫單元測試眉反,這也有利于我們設(shè)計(jì)出好的代碼昙啄。
常見的測試不友好的代碼有下面這 5 種:

  • 代碼中包含未決行為邏輯。所謂的未決行為邏輯就是寸五,代碼的輸出是隨機(jī)或者說不確定的梳凛,比如,跟時間梳杏、隨機(jī)數(shù)有關(guān)的代碼韧拒。
  • 濫用可變?nèi)肿兞俊H肿兞渴且环N面向過程的編程風(fēng)格十性,有種種弊端叛溢。實(shí)際上,濫用全局變量也讓編寫單元測試變得困難劲适。
  • 濫用靜態(tài)方法楷掉。靜態(tài)方法跟全局變量一樣,也是一種面向過程的編程思維霞势。在代碼中調(diào)用靜態(tài)方法烹植,有時候會導(dǎo)致代碼不易測試。主要原因是靜態(tài)方法也很難 mock支示。
  • 使用復(fù)雜的繼承關(guān)系刊橘。如果父類需要 mock 某個依賴對象才能進(jìn)行單元測試鄙才,那所有的子類颂鸿、子類的子類……在編寫單元測試的時候,都要 mock 這個依賴對象攒庵。
  • 高度耦合的代碼嘴纺。如果一個類職責(zé)很重,需要依賴十幾個外部對象才能完成工作浓冒,代碼高度耦合栽渴,那我們在編寫單元測試的時候,可能需要 mock 這十幾個依賴的對象稳懒。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闲擦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌墅冷,老刑警劉巖纯路,帶你破解...
    沈念sama閱讀 216,843評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異寞忿,居然都是意外死亡驰唬,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評論 3 392
  • 文/潘曉璐 我一進(jìn)店門腔彰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叫编,“玉大人,你說我怎么就攤上這事霹抛〈暧猓” “怎么了?”我有些...
    開封第一講書人閱讀 163,187評論 0 353
  • 文/不壞的土叔 我叫張陵上炎,是天一觀的道長恃逻。 經(jīng)常有香客問我,道長藕施,這世上最難降的妖魔是什么寇损? 我笑而不...
    開封第一講書人閱讀 58,264評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮裳食,結(jié)果婚禮上矛市,老公的妹妹穿的比我還像新娘。我一直安慰自己诲祸,他們只是感情好浊吏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著救氯,像睡著了一般找田。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上着憨,一...
    開封第一講書人閱讀 51,231評論 1 299
  • 那天墩衙,我揣著相機(jī)與錄音,去河邊找鬼甲抖。 笑死漆改,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的准谚。 我是一名探鬼主播挫剑,決...
    沈念sama閱讀 40,116評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼柱衔!你這毒婦竟也來了樊破?” 一聲冷哼從身側(cè)響起愉棱,我...
    開封第一講書人閱讀 38,945評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哲戚,沒想到半個月后羽氮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惫恼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評論 2 333
  • 正文 我和宋清朗相戀三年档押,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祈纯。...
    茶點(diǎn)故事閱讀 39,754評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡令宿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出腕窥,到底是詐尸還是另有隱情粒没,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評論 5 344
  • 正文 年R本政府宣布簇爆,位于F島的核電站癞松,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏入蛆。R本人自食惡果不足惜响蓉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哨毁。 院中可真熱鬧枫甲,春花似錦、人聲如沸扼褪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽话浇。三九已至脏毯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間幔崖,已是汗流浹背食店。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留岖瑰,地道東北人叛买。 一個月前我還...
    沈念sama閱讀 47,797評論 2 369
  • 正文 我出身青樓砂代,卻偏偏與公主長得像蹋订,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子刻伊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評論 2 354

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