一文讓你快速上手 Mockito 單元測試框架

前言

在計算機(jī)編程中,單元測試是一種軟件測試方法沥匈,通過該方法可以測試源代碼的各個單元功能是否適合使用聪黎。為代碼編寫單元測試有很多好處罕容,包括可以及早的發(fā)現(xiàn)代碼錯誤备恤,促進(jìn)更改,簡化集成锦秒,方便代碼重構(gòu)以及許多其它功能露泊。使用 Java 語言的朋友應(yīng)該用過或者聽過 Junit 就是用來做單元測試的,那么為什么我們還需要 Mockito 測試框架呢旅择?想象一下這樣的一個常見的場景惭笑,當(dāng)前要測試的類依賴于其它一些類對象時,如果用 Junit 來進(jìn)行單元測試的話生真,我們就必須手動創(chuàng)建出這些依賴的對象沉噩,這其實是個比較麻煩的工作,此時就可以使用 Mockito 測試框架來模擬那些依賴的類柱蟀,這些被模擬的對象在測試中充當(dāng)真實對象的虛擬對象或克隆對象川蒙,而且 Mockito 同時也提供了方便的測試行為驗證。這樣就可以讓我們更多地去關(guān)注當(dāng)前測試類的邏輯长已,而不是它所依賴的對象畜眨。

生成 Mock 對象方式

要使用 Mockito,首先需要在我們的項目中引入 Mockito 測試框架依賴术瓮,基于 Maven 構(gòu)建的項目引入如下依賴即可:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.3.3</version>
    <scope>test</scope>
</dependency>

如果是基于 Gradle 構(gòu)建的項目康聂,則引入如下依賴:

testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3'

使用 Mockito 通常有兩種常見的方式來創(chuàng)建 Mock 對象。

1胞四、使用 Mockito.mock(clazz) 方式

通過 Mockito 類的靜態(tài)方法 mock 來創(chuàng)建 Mock 對象早抠,例如以下創(chuàng)建了一個 List 類型的 Mock 對象:

List<String> mockList = Mockito.mock(ArrayList.class);

由于 mock 方法是一個靜態(tài)方法,所以通常會寫成靜態(tài)導(dǎo)入方法的方式撬讽,即 List<String> mockList = mock(ArrayList.class)蕊连。

2、使用 @Mock 注解方式

第二種方式就是使用 @Mock 注解方式來創(chuàng)建 Mock 對象游昼,使用該方式創(chuàng)需要注意的是要在運行測試方法前使用 MockitoAnnotations.initMocks(this) 或者單元測試類上加上 @ExtendWith(MockitoExtension.class) 注解甘苍,如下所示代碼創(chuàng)建了一個 List 類型的 Mock 對象(PS: @BeforeEach 是 Junit 5 的注解,功能類似于 Junit 4 的 @Before 注解烘豌。):

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
//@ExtendWith(MockitoExtension.class)
public class MockitoTest {

  @Mock
  private List<String> mockList;

  @BeforeEach
  public void beforeEach() {
    MockitoAnnotations.initMocks(this);
  }
}

驗證性測試

Mockito 測試框架中提供了 Mockito.verify 靜態(tài)方法讓我們可以方便的進(jìn)行驗證性測試载庭,比如方法調(diào)用驗證、方法調(diào)用次數(shù)驗證廊佩、方法調(diào)用順序驗證等囚聚,下面看看具體的代碼。

驗證方法單次調(diào)用

驗證方法單次調(diào)用的話直接 verify 方法后加上待驗證調(diào)用方法即可标锄,以下代碼的功能就是驗證 mockList 對象的 size 方法被調(diào)用一次顽铸。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_SimpleInvocationOnMock() {
    mockList.size();
    verify(mockList).size();
  }
}
驗證方法調(diào)用指定次數(shù)

除了驗證單次調(diào)用,我們有時候還需要驗證一些方法被調(diào)用多次或者指定的次數(shù)料皇,那么此時就可以使用 verify + times 方法來驗證方法調(diào)用指定次數(shù)谓松,同時還可以結(jié)合 atLeast + atMost 方法來提供調(diào)用次數(shù)范圍星压,同時還有 never 等方法驗證不被調(diào)用等。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_NumberOfInteractionsWithMock() {
    mockList.size();
    mockList.size();

    verify(mockList, times(2)).size();
    verify(mockList, atLeast(1)).size();
    verify(mockList, atMost(10)).size();
  }
}
驗證方法調(diào)用順序

同時還可以使用 inOrder 方法來驗證方法的調(diào)用順序鬼譬,下面示例驗證 mockList 對象的 size娜膘、addclear 方法的調(diào)用順序。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_OrderedInvocationsOnMock() {
    mockList.size();
    mockList.add("add a parameter");
    mockList.clear();

    InOrder inOrder = inOrder(mockList);

    inOrder.verify(mockList).size();
    inOrder.verify(mockList).add("add a parameter");
    inOrder.verify(mockList).clear();
  }
}

以上只是列舉了一些簡單的驗證性測試优质,還有驗證測試方法調(diào)用超時以及更多的驗證測試可以通過相關(guān)官方文檔探索學(xué)習(xí)竣贪。

驗證方法異常

異常測試我們需要使用 Mockito 框架提供的一些調(diào)用行為定義,Mockito 提供了 when(...).thenXXX(...) 來讓我們定義方法調(diào)用行為巩螃,以下代碼定義了當(dāng)調(diào)用 mockMapget 方法無論傳入任何參數(shù)都會拋出一個空指針 NullPointerException 異常贾富,然后通過 Assertions.assertThrows 來驗證調(diào)用結(jié)果。

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoExceptionTest {

  @Mock
  public Map<String, Integer> mockMap;

  @Test
  public void whenConfigNonVoidReturnMethodToThrowEx_thenExIsThrown() {
    when(mockMap.get(anyString())).thenThrow(NullPointerException.class);

    assertThrows(NullPointerException.class, () -> mockMap.get("mghio"));
  }
}

同時 when(...).thenXXX(...) 不僅可以定義方法調(diào)用拋出異常牺六,還可以定義調(diào)用方法后的返回結(jié)果颤枪,比如 when(mockMap.get("mghio")).thenReturn(21); 定義了當(dāng)我們調(diào)用 mockMapget 方法并傳入?yún)?shù) mghio 時的返回結(jié)果是 21。這里有一點需要注意淑际,使用以上這種方式定義的 mock 對象測試實際并不會影響到對象的內(nèi)部狀態(tài)畏纲,如下圖所示:

mockito_mock_object_thennoaffect.png

雖然我們已經(jīng)在 mockList 對象上調(diào)用了 add 方法,但是實際上 mockList 集合中并沒有加入 mghio春缕,這時候如果需要對 mock 對象有影響盗胀,那么需要使用 spy 方式來生成 mock 對象。

public class MockitoTest {

  private List<String> mockList = spy(ArrayList.class);

  @Test
  public void add_spyMockList_thenAffect() {
    mockList.add("mghio");

    assertEquals(0, mockList.size());
  }
}

端點后可以發(fā)現(xiàn)當(dāng)使用 spy 方法創(chuàng)建出來的 mock 對象調(diào)用 add 方法后锄贼,mghio 被成功的加入到 mockList 集合當(dāng)中票灰。

mockito_mock_object_thenhasaffect.png

與 Spring 框架集成

Mockito 框架提供了 @MockBean 注解用來將 mock 對象注入到 Spring 容器中,該對象會替換容器中任何現(xiàn)有的相同類型的 bean宅荤,該注解在需要模擬特定bean(例如外部服務(wù))的測試場景中很有用屑迂。如果使用的是 Spring Boot 2.0+ 并且當(dāng)前容器中已有相同類型的 bean 的時候,需要設(shè)置 spring.main.allow-bean-definition-overridingtrue(默認(rèn)為 false)允許 bean 定義覆蓋冯键。下面假設(shè)要測試通過用戶編碼查詢用戶的信息惹盼,有一個數(shù)據(jù)庫操作層的 UserRepository,也就是我們等下要 mock 的對象惫确,定義如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@Repository
public interface UserRepository {

  User findUserById(Long id);

}

還有用戶操作的相關(guān)服務(wù) UserService 類手报,其定義如下所示:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@Service
public class UserService {

  private UserRepository userRepository;

  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public User findUserById(Long id) {
    return userRepository.findUserById(id);
  }
}

在測試類中使用 @MockBean 來標(biāo)注 UserRepository 屬性表示這個類型的 bean 使用的是 mock 對象,使用 @Autowired 標(biāo)注表示 UserService 屬性使用的是 Spring 容器中的對象改化,然后使用 @SpringBootTest 啟用 Spring 環(huán)境即可掩蛤。

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@SpringBootTest
public class UserServiceUnitTest {

  @Autowired
  private UserService userService;

  @MockBean
  private UserRepository userRepository;

  @Test
  public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {
    User expectedUser = new User(9527L, "mghio", "18288888880");
    when(userRepository.findUserById(9527L)).thenReturn(expectedUser);
    User actualUser = userService.findUserById(9527L);
    assertEquals(expectedUser, actualUser);
  }
}

Mockito 框架的工作原理

通過以上介紹可以發(fā)現(xiàn), Mockito 非常容易使用并且可以方便的驗證一些方法的行為陈肛,相信你已經(jīng)看出來了揍鸟,使用的步驟是先創(chuàng)建一個需要 mock 的對象 Target ,該對象如下:

public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name);
  }

}

然后我們直接使用 Mockito.mock 方法和 when(...).thenReturn(...) 來生成 mock 對象并指定方法調(diào)用時的行為燥爷,代碼如下:

@Test
public void test_foo() {
  String expectedResult = "Mocked mghio";
  when(mockTarget.foo("mghio")).thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}

仔細(xì)觀察以上 when(mockTarget.foo("mghio")).thenReturn(expectedResult) 這行代碼蜈亩,首次使用我也覺得很奇怪,when 方法的入?yún)⒕谷皇欠椒ǖ姆祷刂?mockTarget.foo("mghio")前翎,覺得正確的代碼應(yīng)該是這樣 when(mockTarget).foo("mghio")稚配,但是這個寫法實際上無法進(jìn)行編譯。既然 Target.foo 方法的返回值是 String 類型港华,那是不是可以使用如下方式呢道川?

Mockito.when("Hello, I am mghio").thenReturn("Mocked mghio");

結(jié)果是編譯通過,但是在運行時報錯:

mockito_when_method_runtime_error.png

從錯誤提示可以看出立宜,when 方法需要一個方法調(diào)用的參數(shù)冒萄,實際上它只需要 more 對象方法調(diào)用在 when 方法之前就行,我們看看下面這個測試代碼:

@Test
public void test_mockitoWhenMethod() {
  String expectedResult = "Mocked mghio";
  mockTarget.foo("mghio");
  when("Hello, I am mghio").thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}

以上代碼可以正常測試通過橙数,結(jié)果如下:

mockito_mock_when_method_pass.png

為什么這樣就可以正常測試通過尊流?是因為當(dāng)我們調(diào)用 mock 對象的 foo 方法時,Mockito 會攔截方法的調(diào)用然后將方法調(diào)用的詳細(xì)信息保存到 mock 對象的上下文中灯帮,當(dāng)調(diào)用到 Mockito.when 方法時崖技,實際上是從該上下文中獲取最后一個注冊的方法調(diào)用,然后把 thenReturn 的參數(shù)作為其返回值保存钟哥,然后當(dāng)我們的再次調(diào)用 mock 對象的該方法時迎献,之前已經(jīng)記錄的方法行為將被再次回放,該方法觸發(fā)攔截器重新調(diào)用并且返回我們在 thenReturn 方法指定的返回值腻贰。以下是 Mockito.when 方法的源碼:

mockito_when_sourcecode.png

該方法里面直接使用了 MockitoCore.when 方法吁恍,繼續(xù)跟進(jìn),該方法源碼如下:

mockito_when_method_mockitocore_sourcecode.png

仔細(xì)觀察可以發(fā)現(xiàn)播演,在源碼中并沒有用到參數(shù) methodCall冀瓦,而是從 MockingProgress 實例中獲取 OngoingStubbing 對象,這個 OngoingStubbing 對象就是前文所提到的上下文對象写烤。個人感覺是 Mockito 為了提供簡潔易用的 API 然后才制造了 when 方法調(diào)用的這種“幻象”咕幻,簡而言之,Mockito 框架通過方法攔截在上下文中存儲和檢索方法調(diào)用詳細(xì)信息來工作的顶霞。

如何實現(xiàn)一個微型的 Mock 框架

知道了 Mockito 的運行原理之后肄程,接下來看看要如何自己去實現(xiàn)一個類似功能的 mock 框架出來,看到方法攔截這里我相信你已經(jīng)知道了选浑,其實這就是 AOP 啊蓝厌,但是通過閱讀其源碼發(fā)現(xiàn) Mockito 其實并沒有使用我們熟悉的 Spring AOP 或者 AspectJ 做的方法攔截,而是通過運行時增強庫 Byte Buddy 和反射工具庫 Objenesis 生成和初始化 mock 對象的古徒。
現(xiàn)在拓提,通過以上分析和源碼閱讀可以定義出一個簡單版本的 mock 框架了,將自定義的 mock 框架命名為 imock隧膘。這里有一點需要注意的是代态,Mockito 有一個好處是寺惫,它不需要進(jìn)行初始化,可以直接通過其提供的靜態(tài)方法來立即使用它蹦疑。在這里我們也使用相同名稱的靜態(tài)方法西雀,通過 Mockito 源碼:

mockito_delegate_mockitocore.png

很容易看出 Mockito 類最終都是委托給 MockitoCore 去實現(xiàn)的功能,而其只提供了一些面向使用者易用的靜態(tài)方法歉摧,在這里我們也定義一個這樣的代理對象 IMockCore艇肴,這個類中需要一個創(chuàng)建 mock 對象的方法 mock 和一個給方法設(shè)定返回值的 thenReturn 方法,同時該類中持有一個方法調(diào)用詳情 InvocationDetail 集合列表叁温,這個類是用來記錄方法調(diào)用詳細(xì)信息的再悼,然后 when 方法僅返回列表中的最后一個 InvocationDetail,這里列表可以直接使用 Java 中常用的 ArrayList 即可膝但,這里的 ArrayList 集合列表就實現(xiàn)了 Mockito 中的 OngoingStubbing 的功能冲九。
根據(jù)方法的三要素方法名方法參數(shù)方法返回值很容易就可以寫出 InvocationDetail 類的代碼跟束,為了對方法在不同類有同名的情況區(qū)分娘侍,還需要加上類全稱字段和重寫該類的 equalshashCode 方法(判斷是否在調(diào)用方法集合列表時需要根據(jù)該方法判斷),代碼如下所示:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class InvocationDetail<T> {

  private String attachedClassName;

  private String methodName;

  private Object[] arguments;

  private T result;

  public InvocationDetail(String attachedClassName, String methodName, Object[] arguments) {
    this.attachedClassName = attachedClassName;
    this.methodName = methodName;
    this.arguments = arguments;
  }

  public void thenReturn(T t) {
    this.result = t;
  }

  public T getResult() {
    return result;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    InvocationDetail<?> behaviour = (InvocationDetail<?>) o;
    return Objects.equals(attachedClassName, behaviour.attachedClassName) &&
        Objects.equals(methodName, behaviour.methodName) &&
        Arrays.equals(arguments, behaviour.arguments);
  }

  @Override
  public int hashCode() {
    int result = Objects.hash(attachedClassName, methodName);
    result = 31 * result + Arrays.hashCode(arguments);
    return result;
  }
}

接下來就是如何去創(chuàng)建我們的 mock 對象了泳炉,在這里我們也使用 Byte BuddyObjenesis 庫來創(chuàng)建 mock 對象憾筏,IMockCreator 接口定義如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public interface IMockCreator {

  <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList);

}

實現(xiàn)類 ByteBuddyIMockCreator 使用 Byte Buddy 庫在運行時動態(tài)生成 mock 類對象代碼然后使用 Objenesis 去實例化該對象。代碼如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class ByteBuddyIMockCreator implements IMockCreator {

  private final ObjenesisStd objenesisStd = new ObjenesisStd();

  @Override
  public <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList) {
    ByteBuddy byteBuddy = new ByteBuddy();

    Class<? extends T> classWithInterceptor = byteBuddy.subclass(mockTargetClass)
        .method(ElementMatchers.any())
        .intercept(MethodDelegation.to(InterceptorDelegate.class))
        .defineField("interceptor", IMockInterceptor.class, Modifier.PRIVATE)
        .implement(IMockIntercepable.class)
        .intercept(FieldAccessor.ofBeanProperty())
        .make()
        .load(getClass().getClassLoader(), Default.WRAPPER).getLoaded();

    T mockTargetInstance = objenesisStd.newInstance(classWithInterceptor);
    ((IMockIntercepable) mockTargetInstance).setInterceptor(new IMockInterceptor(behaviorList));

    return mockTargetInstance;
  }
}

基于以上分析我們可以很容易寫出創(chuàng)建 mock 對象的 IMockCore 類的代碼如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMockCore {

  private final List<InvocationDetail> invocationDetailList = new ArrayList<>(8);

  private final IMockCreator mockCreator = new ByteBuddyIMockCreator();

  public <T> T mock(Class<T> mockTargetClass) {
    T result = mockCreator.createMock(mockTargetClass, invocationDetailList);
    return result;
  }

  @SuppressWarnings("unchecked")
  public <T> InvocationDetail<T> when(T methodCall) {
    int currentSize = invocationDetailList.size();
    return (InvocationDetail<T>) invocationDetailList.get(currentSize - 1);
  }
}

提供給使用者的類 IMock 只是對 IMockCore 進(jìn)行的簡單調(diào)用而已花鹅,代碼如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMock {

  private static final IMockCore IMOCK_CORE = new IMockCore();

  public static <T> T mock(Class<T> clazz) {
    return IMOCK_CORE.mock(clazz);
  }

  public static <T> InvocationDetail when(T methodCall) {
    return IMOCK_CORE.when(methodCall);
  }
}

通過以上步驟氧腰,我們就已經(jīng)實現(xiàn)了一個微型的 mock 框架了,下面來個實際例子測試一下刨肃,首先創(chuàng)建一個 Target 對象:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name);
  }

}

然后編寫其對應(yīng)的測試類 IMockTest 類如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMockTest {

  @Test
  public void test_foo_method() {
    String exceptedResult = "Mocked mghio";
    Target mockTarget = IMock.mock(Target.class);

    IMock.when(mockTarget.foo("mghio")).thenReturn(exceptedResult);

    String actualResult = mockTarget.foo("mghio");

    assertEquals(exceptedResult, actualResult);
  }

}

以上測試的可以正常運行古拴,達(dá)到了和 Mockito 測試框架一樣的效果,運行結(jié)果如下:

mockito_imock_test_pass.png

上面只是列出了一些關(guān)鍵類的源碼真友,自定義 IMock 框架的所有代碼已上傳至 Github 倉庫 imock黄痪,感興趣的朋友可以去看看。

總結(jié)

本文只是介紹了 Mockito 的一些使用方法盔然,這只是該框架提供的最基礎(chǔ)功能桅打,更多高級的用法可以去官網(wǎng)閱讀相關(guān)的文檔,然后介紹了框架中 when(...).thenReturn(...) 定義行為方法的實現(xiàn)方式并按照其源碼思路實現(xiàn)了一個相同功能的簡易版的 imock 愈案。雖然進(jìn)行單元測試有很多優(yōu)點挺尾,但是也不可盲目的進(jìn)行單元測試,在大部分情況下只要做好對項目中邏輯比較復(fù)雜站绪、不容易理解的核心業(yè)務(wù)模塊以及項目中公共依賴的模塊的單元測試就可以了遭铺。


參考文章

Mockito
Objenesis
Byte Buddy

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子魂挂,更是在濱河造成了極大的恐慌甫题,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,686評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涂召,死亡現(xiàn)場離奇詭異坠非,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)芹扭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,668評論 3 385
  • 文/潘曉璐 我一進(jìn)店門麻顶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赦抖,“玉大人舱卡,你說我怎么就攤上這事《佑” “怎么了轮锥?”我有些...
    開封第一講書人閱讀 158,160評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長要尔。 經(jīng)常有香客問我舍杜,道長,這世上最難降的妖魔是什么赵辕? 我笑而不...
    開封第一講書人閱讀 56,736評論 1 284
  • 正文 為了忘掉前任既绩,我火速辦了婚禮,結(jié)果婚禮上还惠,老公的妹妹穿的比我還像新娘饲握。我一直安慰自己,他們只是感情好蚕键,可當(dāng)我...
    茶點故事閱讀 65,847評論 6 386
  • 文/花漫 我一把揭開白布救欧。 她就那樣靜靜地躺著,像睡著了一般锣光。 火紅的嫁衣襯著肌膚如雪笆怠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,043評論 1 291
  • 那天誊爹,我揣著相機(jī)與錄音蹬刷,去河邊找鬼。 笑死频丘,一個胖子當(dāng)著我的面吹牛箍铭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播椎镣,決...
    沈念sama閱讀 39,129評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼诈火,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起冷守,我...
    開封第一講書人閱讀 37,872評論 0 268
  • 序言:老撾萬榮一對情侶失蹤刀崖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拍摇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體亮钦,經(jīng)...
    沈念sama閱讀 44,318評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,645評論 2 327
  • 正文 我和宋清朗相戀三年充活,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜂莉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,777評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡混卵,死狀恐怖映穗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情幕随,我是刑警寧澤蚁滋,帶...
    沈念sama閱讀 34,470評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站赘淮,受9級特大地震影響辕录,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜梢卸,卻給世界環(huán)境...
    茶點故事閱讀 40,126評論 3 317
  • 文/蒙蒙 一走诞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蛤高,春花似錦蚣旱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,861評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至猜欺,卻和暖如春位隶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背开皿。 一陣腳步聲響...
    開封第一講書人閱讀 32,095評論 1 267
  • 我被黑心中介騙來泰國打工涧黄, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赋荆。 一個月前我還...
    沈念sama閱讀 46,589評論 2 362
  • 正文 我出身青樓笋妥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親窄潭。 傳聞我的和親對象是個殘疾皇子春宣,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,687評論 2 351