指南与踩坑:Python 单元测试

本文试图总结编写单元测试的流程,以及自己在写单元测试时踩到的一些坑。如有遗漏,纯属必然,欢迎补充。

目录概览:

  • 编写思想 x 3
  • 编写方法
    • 基本单元测试框架x3
    • 对基于网站框架搭建的网络应用进行单元测试

编写思想

尽可能地按「单元」测试,重点是:保证待测单元内部的所有流程能按设想正确运行、返回预期结果。

假设我们有一个待测程序如下

# to_test.py

def func_01(param0101, param0102):
    # 处理参数 param0101 和 param0102 的代码,如
    ret = param0101 * param0102
    # 假设结果保存在名为 ret 的变量中
    return ret

def func_02(param0201, param0202):
    # 处理参数 param0201 和 param0202 的代码,如
    ret = param0201 / param0202
    # 假设结果保存在名为 ret 的变量中
    return ret
    

def call_other_funcs(param0101, param0102, param0201, param0202):
    # 对传入参数 param0x0y 等进行一些处理后,存到了 param0x0y_changed 中。例如
    param0x0y = param0x0y * 10
    
    ret01 = func_01(param0101_changed, param0102_changed)
    ret02 = func_02(param0201_changed, param0202_changed)
    
    # 对得到的 ret01, ret02 进行一些处理,得到最终返回值 ret。例如
    if ret01 and ret02:
       ret = (ret01, ret02)
    elif ret01 and (not ret02):
       ret = (ret01, 0)
    elif (not ret01) and ret02
       ret = (0, ret02)
    else:
       ret = (0, 0)
    return ret

要关注的点有2:

1. 每个测试仅保证 1 个单元的内部流程正确,即待测单元;这种正确性是不依赖于外部流程的正确性的

所以要假定该单元内调用的外部单元(如引入的模块、函数等)能返回预期结果——这通过所谓的 Mock 来实现,可理解为就是伪造出预期结果;至于那个外部单元能不能真的按给定输入返回预期结果,那是那个外部单元对应的单元测试应该负责的事。

以上述示例程序 to_test.py 为例,本原则所关注的点体现在:

  • 对 call_other_funcs 编写单元测试时,我们仅关注上述除了 ret01 赋值和 ret02 赋值以外的代码是否被正确执行:

    • 我们仅关注 param0101..param0202 在输入后会不会执行 +100 的操作乃至生成对应的 param0101_changed...param0202_changed
    • 我们仅关注当 ret01 和 ret02 得到给定值后会不会执行那套 if-elif-elif-else 以生成我们想要的 (ret01, ret02)、(ret01, 0)、(0, ret02) 三者之一
  • 我们并不关注 ret01、ret02 是怎么由 param0101_changed...param0202_changed 通过 func_01、func_02 生成我们想要的值,那是对 func_01、func_02的测试应该负责的部分

    • 比如当我们想测第一个 if 时,我们就令 func_01 与 func_02 均返回 0;要测第 1 个 elif 时,我们就令 func_01 返回 1, 令 func_02 返回 0;遥测第 2 个 elif 时,我们就令 func_01 返回 0, 令 func_02 返回 1;要测 else 时,我们就令 func_01 与 func_02 均返回 0
    • 假设我们不伪造 func_01 和 func_02 的返回结果,而是直接让参数 param0101_changed...param0202_changed 传给 func_01 和 func_02、由它们来返回想要的结果如 (0, 0),我们实际上测试的不是上述的 call_other_funcs,而是在测下面的代码——多测了这两个函数的内部逻辑,换言之我们不仅仅在保证 1 个单元的正确性,而是在同时保证 3 个单元的正确性
def call_other_funcs_new(param0101, param0102, param0201, param0202):
    # 对传入参数 param0x0y 等进行一些处理后,存到了 param0x0y_changed 中。例如
    param0x0y = param0x0y * 10
    
    ret01 = param0101_changed * param0102_changed  # 我们还测了 func_01 的内部处理逻辑
    ret02 = param0201_changed / param0202_changed  # 我们还测了 func_02 的内部处理逻辑
    
    # 对得到的 ret01, ret02 进行一些处理,得到最终返回值 ret。例如
    if ret01 and ret02:
       ret = (ret01, ret02)
    elif ret01 and (not ret02):
       ret = (ret01, 0)
    elif (not ret01) and ret02
       ret = (0, ret02)
    else:
       ret = (0, 0)
    return ret

2. 测试要尽可能覆盖到所有语句

仍然以上述函数为例,这里并不只是说测试需要覆盖所有的 if-else 分支,而更着重于强调对上一条原则的配合,即:尽管我们要伪造一些函数的值,但我们也要保证对应的函数调用了指定的参数。

这是因为函数调用的参数可能依赖于调用函数前的代码,因此确保函数调用了指定参数,这种行为则确保了函数调用前那些(涉及到参数的)代码能够正确执行。例如上述代码中, 如果我们仅仅令 funcs_01 的返回值为某值,而没有去检查 funcs_01 到底调用的参数是不是我们预期的参数,那么实际上我们就并没有测试到像 param0101 = param0101 * 10 这样的代码。

上述 2 点是基本原则。在此之上,根据我踩的坑,还有 1 点想补充:

1. 测试要写得「傻」一点

感谢首席测试小姐姐指出:测试不仅仅是为了保证功能正确,也是一份「代码阅读指南」——即当待测单元的行为不容易理解时,用户可以通过阅读这份代码对应的单元测试来理解程序行为。

仍以上述对 to_test.py 的测试为例。(下面的 @mock.patch.object 与 mock_funcs_0x.return_value 配合,实现「伪造函数值」)

坏样例:

from unittest import mock
import to_test

class ToTestTestCase(unittest.TestCase):

    # ...其他测试函数...
   
    @mock.patch.object(to_test, 'funcs01')
    @mock.patch.object(to_test, 'funcs02')
    def test_call_other_funcs(self, mock_funcs_01, mock_func_02):
        funcs_ret_values = [
            {"funcs_01": 1, "funcs_02": 1},
            {"funcs_01": 1, "funcs_02": 0},
            {"funcs_01": 0, "funcs_02": 1},
            {"funcs_01": 0, "funcs_02": 0}
        ]
        for funcs_ret in funcs_ret_values:
            mock_funcs_01.return_value = funcs_ret["funcs_01"]
            mock_funcs_02.return_value = funcs_ret["funcs_02"]
            # 剩下的测试语句……

好样例:

from unittest import mock
import to_test

class ToTestTestCase(unittest.TestCase):

    # ...其他测试函数...
   
    @mock.patch.object(to_test, 'funcs01')
    @mock.patch.object(to_test, 'funcs02')
    def test_call_other_funcs_if(self, mock_funcs_01, mock_func_02):
        mock_funcs_01.return_value = 1
        mock_funcs_02.return_value = 1
        # 剩下的测试语句……
        
    @mock.patch.object(to_test, 'funcs01')
    @mock.patch.object(to_test, 'funcs02')
    def test_call_other_funcs_elif_1(self, mock_funcs_01, mock_func_02):
        mock_funcs_01.return_value = 1
        mock_funcs_02.return_value = 0
        # 剩下的测试语句……
        

    @mock.patch.object(to_test, 'funcs01')
    @mock.patch.object(to_test, 'funcs02')
    def test_call_other_funcs_elif_2(self, mock_funcs_01, mock_func_02):
        mock_funcs_01.return_value = 0
        mock_funcs_02.return_value = 1
        # 剩下的测试语句……
        

    @mock.patch.object(to_test, 'funcs01')
    @mock.patch.object(to_test, 'funcs02')
    def test_call_other_funcs_else(self, mock_funcs_01, mock_func_02):
        mock_funcs_01.return_value = 0
        mock_funcs_02.return_value = 0
        # 剩下的测试语句……

第一种写法看起来更简洁,更「模块化」,对于单个函数的测试被封装到了同一个函数中;但首先要面临的问题就是:

每次你在阅读测试函数是如何测试目标函数时,就要到上述的 list(如这里的 funcs_ret_values)中去查对应的函数到底被伪造成了什么值。

乍一看,这在需要伪造的函数值较少时看起来还不是大问题;但当需要伪造的函数数量多起来时,上面的 list of dict 就会变得冗长无比,非常不容易阅读。

更严重的是第二个问题:

设想一种情景:你需要测试函数内部的 2 条不同逻辑 A 和 B,而这些不同逻辑会返回同样的值 a。那么,由于测试是在for循环中进行的,当你发现希望返回 a 的时候没有返回 a,你就不知道到底是在测逻辑 A 时出了错,还是在测逻辑 B 时出了错。于是你可能不得不非常仔细地去检查样例,手动再模拟一遍测试的过程,而且还要手动模拟 2 次:既要考虑模拟逻辑 A,也要考虑模拟逻辑 B。这加大了debug测试程序的难度。

第二种写法虽然看上去更琐碎,但由于测试粒度比较小,上述这两个问题就都不复存在了。

编写方法

基本单元测试框架

基本测试框架如下。先阅读,再解释:

假设我们有一个类似刚才的 to_test.py 的待测函数 your_mod_name.py

# 引入单元测试模块(unittest)和伪造模块(mock)
import unittest
from unittest import mock

import your_mod_name

class YourModNameTestCase(unittest.TestCase):


    def setUp(self):
        """
        若每个单元测试前都要用到同一组数据,则在这里编写,如
        
        self.var00 = val00
        self.var01 = val01
        
        不一定要有
        """
        pass
       
         
    def test_funcs_01(self):
        """
            测试 funcs_01
            
            所有要进行通过自动化测试框架的运行都以 test_ 开头
        """
        actual_output = your_mode_name.funcs_01(5, 2)
        self.assertEqual(
            10,
            actual_output
        )


    def test_funcs_02(self):
        """
            测试 funcs_02
        """
        actual_output = your_mode_name.funcs_02(9, 3)
        self.assertEqual(
            3,
            actual_output
        )


    @mock.patch.object(your_mod_name, 'funcs_02')
    @mock.patch.object(your_mod_name, 'funcs_01')
    def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
        mock_funcs_01.return_value = (5 * 10) * (2 * 10)
        mock_funcs_02.return_value = (9 * 10) / (3 * 10)
        actual_output = your_mode_name.funcs_01(5, 2, 9, 3)
        mock_funcs_01.assert_called_with(5 * 10, 2 * 10)
        mock_funcs_02.assert_called_with(9 * 10, 3 * 10)
        self.assertEqual(
            ((5 * 10) * (2 * 10), (9 * 10) / (3 * 10)),
            actual_output
        )

    # 对 call_other_funcs 其他分支的测试
    #
    # ……
    #
    # 对其他函数的测试

以下解释上述代码

1. 最简单的测试

对于像 funcs_01 和 funcs_02 这样的函数写测试是非常简单的:我们只要

  1. 引入待测模块
  2. 把参数传给 待测模块.待测函数,取得返回值 actual_output 即为实际输出
  3. 使用 assertEuqal 之类以 assert 开头的函数来断言:实际行为与预期行为一致。通常至少用 assertEqual 来断言:实际输出(actual_output)与预期输出一致;在安排「实际输出」与「预期输出」在 assertEqual 中的参数顺序时,我的做法是:第一个参数是「预期输出」,第二个参数是「实际输出」;理由是「预期输出」的形状和长度是固定的,「实际输出」的形状和长度通常会有各种变化(当测试出错或函数没有执行预期行为时),把「实际输出」安排在后面,在调试测试函数时,我们的视线关注点是固定的。
  4. 对于比较复杂的函数如 call_other_funcs 的测试,可能还需要使用 assert_called_with 等方法断言函数调用的参数,见 https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called_with 。当需要断言多次调用时(例如同一个函数 funcs_01 被调用了 3 次,每次传入了不同参数),可以考虑使用 assert_has_calls,见 https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_has_calls
  5. 其他的断言方法可见 https://docs.python.org/3/library/unittest.html#assert-methods 对于 mock 对象的断言方法可见 https://docs.python.org/3/library/unittest.mock.html#the-mock-class

2. 如何伪造一个对象

2.1 通用的 mock 框架

一般来说,我们把「伪造」称为 mock,因为这就是 Python 中的伪造类的名字。

可以通过装饰器 @mock.patch.object 的写法「制造」 mock 对象,见 https://docs.python.org/3/library/unittest.mock.html#patch-object

以上述写法为例:

@mock.patch.object(your_mod_name, 'funcs_02')
@mock.patch.object(your_mod_name, 'funcs_01')
def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):

上面这三句的意思是:

把函数 your_mod_name.funcs_01 伪造成 mock_funcs_01,把 your_mod_name.funcs_02 伪造成 mock_funcs_02。这里的 mock_funcs_01 和 mock_funcs_02 的变量名没有特别的规定,这和一般的变量命名没有两样,也可以命名为 mock_weird_name_007, mock_I_dont_know_why_101 等奇奇怪怪的名字。现在这样命名只是为了方便理解。

在「制造」出 mock 对象后,我们要给 mock 对象赋值,因为伪造传值才是我们的实际目标。以上述代码为例,该目标通过这两句来完成:

mock_funcs_01.return_value = (5 * 10) * (2 * 10)
mock_funcs_02.return_value = (9 * 10) / (3 * 10)

注意:上面这两句,即所有伪造值的语句,都要在调用函数的语句前执行,即下面这句之前执行上述两句:

actual_output = your_mode_name.funcs_01(5, 2, 9, 3)

而 return_value 也可以在装饰器中就指定,例如:

@mock.patch.object(your_mod_name, 'funcs_02', return_value=(9 * 10) / (3 * 10))
@mock.patch.object(your_mod_name, 'funcs_01', return_value=(5 * 10) * (2 * 10))
def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
    # 此后就不用再写 mock_funcs_01.return_value = (5 * 10) * (2 * 10) 这样的句子了

除了通过装饰器来伪造,还可以通过上下文管理器(context manager)的方法来伪造 。同样是类似上述代码,可以写为:

@mock.patch.object(your_mod_name, 'funcs_02')
@mock.patch.object(your_mod_name, 'funcs_01')
def test_call_other_funcs_branch_01(self, mock_funcs_01, mock_funcs_02):
    with mock.patch.object(your_mod_name, 'funcs_01', \
          return_value=(5 * 10) * (2 * 10)) as mock_funcs_01,
         mock.patch.object(your_mod_name, 'funcs_02', \
          return_value=(9 * 10) / (3 * 10)) as mock_funcs_02:

        actual_output = your_mode_name.funcs_01(5, 2, 9, 3)
        mock_funcs_01.assert_called_with(5 * 10, 2 * 10)
        mock_funcs_02.assert_called_with(9 * 10, 3 * 10)
        self.assertEqual(
            ((5 * 10) * (2 * 10), (9 * 10) / (3 * 10)),
            actual_output
        )

2.2 特殊语句顺序

所有的伪造值语句,必须要在调用 actual_output 的赋值语句(即实际调用目标函数)之前执行。

所有的断言语句,包括 assertEqual 或 assert_called_with,必须要在调用 actual_output 的赋值语句(即实际调用目标函数)之后执行。这是因为需要断言的值、对象都要在函数执行后才会产生(需要断言的参数调用也是一种值,也要在函数执行后,才会在内存中留下「痕迹」,在执行之前,程序无法知道待测函数内部调用的其他函数到底调用了什么参数)。

要注意:调用 assert_called_with 的一定是某个 mock 对象而非 self,这是与 assertEqual 最大的区别

2.3 使用场景

更具体地说,分为 2 种情况:(1)在诸多测试用例中,每个对象的返回值之间各自独立;(2)这些返回值之间符合某种函数关系

根据这 2 种情况,对应的有 4 种伪造对象的写法。其中 2.3.1 对应第 (1) 种情况,2.3.2~2.3.4 对应第 (2) 种情况:

2.3.1 一般外部模块、网络连接、数据库连接

常见于伪造一般外部模块(自己或团队其他成员写的模块、开源库模块等)、数据库连接、网络连接的返回结果。都是类似上面对 funcs_01 的 mock 方法。给出 2 个在数据库连接和网络连接方面的 mock 示例代码:

假设使用数据库连接的原始代码为:

# your_mod_name.py

import db_conn

# some other function code ...

def funcs_with_db_connection(params):
    # some code ...
    answers = db_conn.query(sql)  # sql 是指定的 SQL 语句字符串
    # some code to process answers
    # 假定 ret 是返回变量
    return ret

则对应的 mock 代码为:

# 数据库连接
import your_mod_name

@mock.patch.object(your_mod_name.db_conn, 'query', return_value=['000001', '000002'])
def test_funcs_with_db_connection(self, mock_db_conn):
    # params 是参数
    # 在有了上面
    actual_output = your_mod_name.funcs_with_db_connection(params)

网络连接则以 requests.get 为例:

# your_mod_name.py

import requests

# some other function code ...

def funcs_with_requests(params):
    # some code ...
    answers = requests.get('your_url_to_site')  # 从 your_url_to_site 获取信息
    # some code to process answers
    # 假定 ret 是返回变量
    return ret

这时候返回的对象可能有多个属性如 status_coe 和 text,而且都要用上。那么此时对应的 mock 代码可写成:

@mock.patch.object(your_mod_name.requests, 'get')
def test_funcs_with_requests(self, mock_requests_get)
    mock_response = mock.Mock()
    mock_response.status_code = status
    mock_response.text = {'key01': 'val01', 'key02': 'val02'}
    mock_requests.return_value = mock_response

2.3.2 伪造成一个指定函数:转发输入

相当于把原始函数的输入值「转发」到指定函数上。例如原始代码为:

# your_mod_name.py

def format_answers(raw_string):
    # 处理 raw_string 非常复杂的处理逻辑
    # 假设处理完后保存到 good_string 中
    return good_string
    
def funcs_with_format_answers(params):
    # 某些代码生成了原始答案字符串 raw_string
    answers = format_answers(raw_string)
    # some code ... 返回 ret
    return ret

比如在测试函数中,我多次调用了该函数,但我不想对每次 mock 都指定一个值,那么可以这么做:

import your_mod_name

def mock_format_answers(input_string):
    return input_string
    
@mock.patch.object(your_mod_name, 'format_answers', mock_format_answers):
def test_funcs_with_format_answers(self):
    # some code for testing

注意到当我们指定了转发目标后,实际上指定了「制造」的 mock 对象为我们设计好的函数,这样就不需要在函数头中再写 mock_format_answers 了(如果写,反而会报错)。

2.3.3 从一个伪造类生成一个伪造对象

设想一个情况:你在原始函数中调用了某个类 ClassA 生成了实例 instance_A,并在原始函数中使用了该类的多个方法。那么一次次 mock 这个函数的一个个方法,可能看着或写着繁琐。在这种情况下,我们就可以通过将对应的类转发到我们设计好的伪造类上,并在伪造类下定义需要 mock 的方法,从而 mock 一个类就相当于 mock 了和该类相关的所有方法。

一个简单的例子是:某个函数内部调用了 time 这个类的 time() 和 sleep() 方法,例如:

# your_mod_name.py

import time

def funcs_with_time(params):
    t_start = time.time()
    # some code ...
    t_cost = time.time() - t_start
    ret = []
    while t_cost <= 10:
       # do something
       time.sleep(0.5)
       t_cost = time.time() - t_start
       if t_cost > 5:
           ret.append('good')
           
     return ret

在我们编写测试的时候,如果不将 time 这个类 mock 掉,那么程序的行为就不可预测:程序运行时是一个随机行为,我们如果不能「控制时间」,就不能保证测试函数在测试时能走到目标函数中的指定分支。

我们只要在测试函数中这么写即可:

import your_mod_name

class MockTime(object):
    """                                                                                                                                                                                       
        用于 time 的 Mock 类
    """
       
    def __init__(self):
        self.time_count = -0.5  # 配合 time() 方法使 ts_start = 0
       
    def time(self):
        """
            每次对象被调用时会运行这里的代码。
        """
        if self.time_count == -0.5:
            self.time_count = 0.0
        return self.time_count
       
    def sleep(self, gap):
        """
            0.5 的增量保证能在 cost 超过 10 之前触发:销毁 Token,返回 URL
        """
        self.time_count += 0.5
        return
        
mock_time_helper = MockTime()


class YourModNameTestCase(unittest.TestCase):

    # some code for testing other functions ...

    @mock.patch.object(your_mod_name, 'time', mock_time_helper)
    def test_funcs_with_time(self):
        # 这样就可以将不可控的「程序运行时」变为可控的「计数器」
        # some code for testing ...

2.3.4 如何伪造内置函数(built-in functions)的返回结果,如open(path_to_file).readlines()

这其实是 2.3.3 这个情况的一个特例,但也是一个容易让人抓狂的点。比如有时候我们需要测试的函数内有一个 open 函数,在打开文件后还调用了 readlines() 方法。那么我们如何 mock 掉 open?或者 open 返回的对象类名是啥,我能不能去 mock 那个类对应的 readlines() 方法?其实这个问题的关键在于:内置函数的类是什么?

答案是:builtins https://docs.python.org/3/library/builtins.html

那么之后就能够像 2.3.3 一样去处理了。

例如原始代码是

# your_mod_name.py

def funcs_with_open(params):
    # some code ...
    tokens = [line.strip('\n') for line in open('path_to_file').readlines()]
    return tokens

对应的测试函数中可以这么写

import unittest
import builtins

import your_mod_name


class MockOpen(object):
    """  
        内置函数 open 的 mock 类
    """
    def __init__(self, data):
        assert isinstance(data, list), '请输入一个列表: {}'.format((data))
        self.data = data 
 
    def __call__(self, blabla):
        return self 
 
    def readlines(self):
        return self.data

class YourModNameTestCase(unittest.TestCase):

    # some code to test other functions
    
    def test_funcs_with_open(self):
        with mock.patch('builtins.open', MockOpen(['test00\n', 'test01\n']))
            # some code to get params
            actual_output = your_mod_name.funcs_with_open(params)
            self.assertEqual(
                ['test00', 'test01'],
                actual_output
            )

对基于网站框架搭建的网络应用进行单元测试

和编写网络应用一样:编写网络应用(即涉及到网络通信的程序)的单元测试,与编写一般程序的单元测试基本一致,最大的差别就在于:网络应用和网络应用的单元测试一般需要额外关注:

  • 路由:要通过哪个URI进行数据操作,例如要从哪里去GET数据、把数据POST到哪里(一般必须处理)
  • 状态码:返回状态的设置和捕捉(不一定要捕捉或设置)

这里就不做太多展开,在 2 个框架下各给出 1 个例子并做简要解释,更多情况请参阅对应的文档,或等我日后填坑(然后可能就不知不觉弃坑了?)

[Flask]

更多有关 Flask 的测试方法,见

https://pythonhosted.org/Flask-Testing/
http://flask.pocoo.org/docs/0.12/testing/

原程序

@app.route('/userapi/get_phone_number', methods=['POST'])
def get_phone_number():
    # some code to get phone number

对应测试程序中如何调用该程序:

# request_data 是已经处理过的要 POST 的 JSON
rv = self.client.post('/userapi/get_phone_number',  data=request_data)

注意到这里的 '/userapi/get_phone_number' 就是调用路由

[Tornado]

更多有关 Tornado 的测试方法,见 http://www.tornadoweb.org/en/stable/testing.html

原项目中由这个程序指定了程序路由:

# server.py

class Application(tornado.web.Application):  

    def __init__(self):
        handlers = [
            # some other handlers ...
            (r"/qaapi/qa", RESTfulAPIHandler)  # 已有 RESTfulAPIHandler.py 是对应应用
        ]

        tornado.web.Application.__init__(self, handlers)
        
        # some other codes ...

要测试时:

uri = '/qaapi/qa?userid={}&token={}'.format(userid, token)
data = get_data()
response = self.fetch(uri, method="POST", body=data)

self.assertEqual(400, response.code)
self.assertEqual('expected_output', response.buffer.getvalue())
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容