手把手教你用Python实现接口自动化测试-开篇

       本文是《手把手教你用Python实现接口自动化测试》这一系列文章的开篇,笔者将从本文开始给大家介绍一下如何使用Python实现接口自动化测试,希望能起到一个抛砖引玉的作用。

       在《手把手教你用Python实现接口自动化测试》这一系列文章将会分成3个部分:
              1. 开篇:介绍Python实现接口自动化的实现思路,项目结构以及展示最终实现的效果;
              2. 支线文章:针对项目中所用到的知识点分别进行展开介绍;
              3. 完结篇:我将在最后一篇文章中对项目中各个细节进行详细阐述。

前言

       开篇文章中我们先主要来说一说在开始写实际业务代码之前的一些准备工作,介绍一下使用Python实现接口自动化的实现思路。在这个系列文章里不会教您Python的基础知识,如果您知道一些简单的编码知识或者进行一定的Python基础学习后会有助于您对本系列文章的阅读。

一、为什么需要实现接口自动化测试?

首先,我们先看一下正常的接口测试流程是什么?

常规测试流程

       这其中可以实现接口测试的工具有很多,例如:如postmanjmeterfiddler等等,这些工具使用起来非常方便,功能也很多。
常用测试工具

那么为什么还要写代码实现接口自动化呢?

  • 工具虽然方便,但也其不足之处:

1. 测试数据不可控制

接口测试本质其实是对数据的验证:我们调用接口,输入接口请求需要的一些数据,接口返回一些数据,然后我们针对接口返回数据验证正确性。在用工具运行测试用例之前不得不手动向数据库中插入测试数据。这样我们的接口测试是不是就没有那么“自动化了”。

2. 回归测试耗时

这是功能测试的一大硬伤,测试人员往往无法面对成百上千条测试case的回归需求,而大部分开发在完成一个改动时都会捎带一句“全量回归一下,看看有没有其他问题”。其次即使我们的测试人员比较给力认真,完成了回归测试,但这其中难免不会有失误或者遗漏之处。机器却能任劳任怨的执行每一条case且保证每次都能按照流程进行测试。

3. 扩展能力不足

当我们在享受工具所带来的便利的同时,往往也会受制于工具所带来的局限。例如,我想将测试结果生 成 HMTL 格式测试报告,我还想将测试报告发送到指定邮箱等等,这些需求都是工具难以实现的。

那么我们期望的接口自动化测试流程是什么样的?

期望测试流程

本系列文章中介绍的接口自动化测试框架还能做什么?

       1:测试数据源可以是多样性的


数据源多样性

       2:多种结果验证方式


多种结果验证方式

       当然我们还可以将测试结果处理为HTML格式的报告以邮件的形式发送至指定邮箱。

       那么接下来,我们就根据这样的过程来一步步搭建我们的框架。在这个过程中,我们需要做到业务和数据的分离,这样才能灵活,达到我们实现接口自动化目的。

二、测试需求梳理

       无论是正常测试还是实现接口测试自动化,我们都应该优先将需求梳理清晰,这样才能在执行的过程中确保不会有偏差。

       本文将以QQ音乐PC版歌手列表排行榜相关功能做示例,示例截图如下:
QQ音乐PC版首页传送门

QQ音乐PC版-歌手首页

QQ音乐PC版歌手列表.jpg

通过简单的页面分析,我们可以获取歌手列表相关接口信息:

1. 歌手列表首页

1.1 歌手列表首页接口请求信息

通过cur_page字段的值控制请求页数,一页返回80条

第一页

https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI1473476396452713&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":-100,"sin":0,"cur_page":1}}}

第二页

https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI9803531398820899&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":-100,"sin":80,"cur_page":2}}}

1.2 歌手列表首页接口返回信息
歌手列表接口返回信息

2. 歌手热门歌曲列表

2.1 周杰伦-热门歌曲列表接口请求信息

通过singermid字段的值控制请求的歌手歌曲列表
通过sin,num字段的值控制请求列表数量,默认返回10条

https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI3347917939773577&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singer":{"method":"get_singer_detail_info","param":{"sort":5,"singermid":"0025NhlN2yWrP4","sin":0,"num":10},"module":"music.web_singer_info_svr"}}

2.2 周杰伦-热门歌曲列表接口返回信息
当前歌手热门歌曲列表相关返回信息

3. 歌手专辑列表

3.1 周杰伦-专辑列表接口请求信息

通过singermid字段的值控制请求的歌手歌曲列表
通过begin,num字段的值控制请求列表数量,默认返回5条

https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI3111122848094381&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singerAlbum":{"method":"get_singer_album","param":{"singermid":"0025NhlN2yWrP4","order":"time","begin":0,"num":5,"exstatus":1},"module":"music.web_singer_info_svr"}}

3.2 周杰伦-专辑列表接口返回信息

当前歌手发表专辑列表相关信息

QQ音乐PC版-排行榜首页
QQ音乐PC版排行榜首页

让我们再通过简单的页面分析,我们可以获取排行榜相关接口信息:

4. 排行榜

4.1 流行指数榜-歌曲列表接口请求信息

通过topId字段的值控制请求的榜单列表
通过offset,num字段的值控制请求列表数量,默认返回20条

https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI2506723965565383&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={"detail":{"module":"musicToplist.ToplistInfoServer","method":"GetDetail","param":{"topId":4,"offset":0,"num":20,"period":"2019-08-14"}},"comm":{"ct":24,"cv":0}}

4.2 流行指数榜-歌曲列表接口请求信息
流行指数榜排行榜接口返回信息

最后,通过以上简单的分析,我们设定有如下测试需求:

测试用例设计

三、项目搭建

       我们把搭建一个项目比喻成建造一间房屋,其中我们需要先为房屋建立基础和框架(项目结构)就像是先建成一座毛坯房。
我们在建造房屋时所使用到的工具:
  • 编程语言:Python 3.7
  • 编译器: Pycharm
  • 项目框架:Flask
  • 测试框架:Unittest
使用Pycharm创建Flask项目后,我们建立如下项目结构:
项目结构
********项目中各个文件作用********

common ------存放公共的方法文件
config ------存放配置文件
testcase ---存放具体的测试case
testdata ---存放相关测试数据
testresult ---存放测试报告和日志文件
testrunner ---用例执行的入口文件
util ----------私有工具文件
app.py -------接口入口文件

现在我们有了一间光秃秃的毛坯房,里面什么都没有,那么接下来我们需要对其进行装修使其拥有较为舒适的居住条件。

1. 首先我们从比较容易的config文件入手

config文件目录

config目录下有2个.ini信息配置文件,以及2个用于读取配置文件信息的.py文件。

1.1 配置文件信息

commoninfo.ini文件配置信息如下(配置了各个api接口的请求参数信息)

[UrlInfo_QQMusic]---接口请求Url信息分组标识
apibaseurl = https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI1473476396452713&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data=
data_singerlist = {"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":-100,"sin":0,"cur_page":1}}}
data_songlist = {"comm":{"ct":24,"cv":0},"singer":{"method":"get_singer_detail_info","param":{"sort":5,"singermid":"0025NhlN2yWrP4","sin":0,"num":10},"module":"music.web_singer_info_svr"}}
data_albumlist = {"comm":{"ct":24,"cv":0},"singerAlbum":{"method":"get_singer_album","param":{"singermid":"0025NhlN2yWrP4","order":"time","begin":0,"num":5,"exstatus":1},"module":"music.web_singer_info_svr"}}
data_weeklist = {"comm":{"ct":24,"cv":0},"request":{"method":"get_history_key_list","param":{"periods":5},"module":"video.VideoRankServer"}}
data_toplist = {"detail":{"module":"musicToplist.ToplistInfoServer","method":"GetDetail","param":{"topId":4,"offset":0,"num":20,"period":"2019-08-14"}},"comm":{"ct":24,"cv":0}}

dbconfig.ini文件配置信息如下(配置了数据库连接参数)

[DataBase_ConnectInfo]---数据库连接参数分组标识
host = **********你需要操作的数据库DB连接串
port = **********DB端口号
dbname = ********DB名称
username = ******DB连接账号
password = ******DB连接密码
1.2 读取配置文件

读取配置文件信息思路
使用configparser对象在对应配置文件中根据分组标识获取相关配置参数
readdbconfig.py为例,其读取dbconfig.ini文件信息的代码如下:

        currentfile_path = os.path.split(os.path.realpath(__file__))[0]
        """拼接config.ini文件的路径地址"""
        dbconfig_path = os.path.join(currentfile_path, "dbconfig.ini")
        """实例化configParser对象"""
        self.cf = configparser.ConfigParser()
        """读取config.ini文件"""
        self.cf.read(dbconfig_path, encoding="utf-8")  
         """根据配置文件中的分组标识获取配置参数信息"""
        connectinfo = dict(self.cf.items('DataBase_ConnectInfo'))  

2. 下面我们看看common目录下面都有哪些公共方法

common文件目录
2.1 excutesql.py文件用于数据库中进行sql语句的执行操作

该封装方法进行了数据库的连接操作,外部调用只需传入需要执行的sql语句即可

2.2 getlogger.py文件用于创建logger对象

该封装方法创建了一个logger对象,供所有case执行时进行相关信息日志的记录

2.3 gettestdata.py文件用于获取case执行时的测试数据
该封装方法是我们实现数据分离的核心

主要通过获取api接口/mysql/excel文件/config配置文件中的数据,然后将数据转成我们case执行时需要的数据格式

当前项目中我每个接口分别使用了不同的数据源获取方式进行演示,在实际项目中可根据情况选择相应的数据源获取方式。

2.4 MyHTMLTestRunner.py文件用于将测试结果生成*.html格式的测试报告

该文件为网上其他大神的作品,我进行了些许的改动,就不提供源码了,大家可自行google即可

2.5 sendemail.py文件用于将测试结果邮件的形式发送至指定邮箱地址

该封装方法是将我们测试结果(包含*.html*.log附件)通过邮件的形式发送至指定接收人

其中邮件收件人:recipients、邮件抄送人:cc信息,我这边是通过在上文commoninfo.ini文件中进行配置,使用configparser对象进行读取

#commoninfo.ini中信息---多个收件人、抄送人以分号隔开
[EMAIL_OUTLOOK]------邮件配置参数信息分组标识
recipients = ***1@***.***; ***2@***.***;
cc = ***1@***.***; ***2@***.***;

3. 我们现在看看测试数据testdata目录有哪些内容

testdata目录下一般存放的是一些静态的 测试数据文件;文件类型不固定,如:.txt.xls.xml.json都能支持;
思路就是通过各个文件的类库方法从文件中读写数据后,通过gettestdata.py封装成我们最终需要的测试数据格式。

testdata文件目录

4. util中放的是一些私有工具类

util中的工具类一般都是针对当前项目封装的一些测试工具类,对于别的项目可能就不太适用。
本文中笔者针对excel文件进行读写封装了一个简单工具类operateexcel.py

util目录

5. 实际的测试case文件

    上面我们有看到测试数据参数的设置,测试数据的获取方式等等,但这些都是前置条件,而实际的测试case是什么样的呢?

我们将测试用例case维护在testcase文件目录下

testcase文件目录

首先我这边根据测试接口类型进行了分类:排行榜相关接口、歌手相关接口;
然后每个接口单独一个.py文件进行具体测试case的编写。

本文编写case时采用的是python自带的unittest测试框架对case进行驱动执行。

每个case文件在被执行之前需求进行测试数据的初始化;
测试数据通过parameterized进行参数化传参;
使用assertEqual进行断言验证。

singerlist.py为例:该文件为“验证每个歌手基本信息中各个字段的信息”测试case

    @parameterized.expand(testdata['singerinfo_list'])
    def test_Country_Null(self, singerinfo):
        """
        验证每个歌手信息中的country不为空
        :param singerinfo:
        :return:
        """
        country = singerinfo['country']
        if country:
            self.assertEqual(True, True)
        else:
            print(singerinfo)
            self.logger.info('singerinfo_test_Country_Null:' + str(singerinfo))
            self.assertEqual(True, False)

    @parameterized.expand(testdata['singerinfo_list'])
    def test_Singer_Id_Type(self, singerinfo):
        """
        验证每个歌手信息中的singer_id数据类型为数字
        :param singerinfo:
        :return:
        """
        singer_id = singerinfo['singer_id']
        if singer_id.__class__.__name__ == 'int':
            self.assertEqual(True, True)
        else:
            print(singerinfo)
            self.logger.info('singerinfo_test_Singer_Id_Type:' + str(singerinfo))
            self.assertEqual(True, False)

    @parameterized.expand(testdata['singerinfo_list'])
    def test_Singer_Mid_Len(self, singerinfo):
        """
        验证每个歌手信息中的singer_mid数据长度
        :param singerinfo:
        :return:
        """
        singer_mid = singerinfo['singer_mid']
        if len(singer_mid) == 14:
            self.assertEqual(True, True)
        else:
            print(singerinfo)
            self.logger.info('singerinfo_test_Singer_Mid_Len:' + str(singerinfo))
            self.assertEqual(True, False)

    @parameterized.expand(testdata['singerinfo_list'])
    def test_Singer_Name_Type(self, singerinfo):
        """
        验证每个歌手信息中的singer_name数据类型
        :param singerinfo:
        :return:
        """
        singer_name = singerinfo['singer_name']
        if singer_name.__class__.__name__ == 'str':
            self.assertEqual(True, True)
        else:
            print(singerinfo)
            self.logger.info('singerinfo_test_Singer_Name_Type:' + str(singerinfo))
            self.assertEqual(True, False)

    @parameterized.expand(testdata['singerinfo_list'])
    def test_Singer_Pic_Info(self, singerinfo):
        """
        验证每个歌手信息中的singer_pic数据内容
        :param singerinfo:
        :return:
        """
        singer_pic = singerinfo['singer_pic']
        singer_mid = singerinfo['singer_mid']
        exceptvalue = 'http://y.gtimg.cn/music/photo_new/T001R150x150M000' + singer_mid + '.webp'
        if singer_pic == exceptvalue:
            self.logger.info('singerinfo_test_Singer_Pic_Info:' + str(singerinfo))
            self.assertEqual(True, True)
        else:
            print(singerinfo)
            self.assertEqual(True, False)

四、测试case的执行

1. 如何执行维护的测试case

       经过前面的操作,我们现在有了一间装修完的房子,里面有家具家电,现在我们要做的就是给它上通上水电煤,让它能运转起来满足我们持续居住的条件。同样,我们的测试case也需要被执行才能实现其存在的意义,下面我们来看看我们的测试case如何被驱动执行的。

我们将测试用例执行文件维护在testrunner文件目录下
testrunner文件目录

我们可以在caserunner_myhtml.py文件中定制我们的case执行方式:

  1. 执行所有接口的测试case
  2. 根据接口名称执行测试case
    def runall(self):

        """1.根据当前文件路径获得case执行时的路径、报告名称信息"""
        pathinfo_dict = TestData().get_pathinfo_dict(__file__)

        """2.初始化测试套件"""
        testsuite = unittest.TestSuite()

        """3.遍历appidlist中的应用appid,获取对应应用下所有接口case,并添加进测试套件testsuite中"""
        global discover, case_list
        case_list = []  # 当前appid下对应的测试用例列表
        """3-1.拼接当前appid的用例路径"""
        filepath_testcase = pathinfo_dict['filepath_testcase']
        """3-2.使用os.walk方法遍历得到所有文件名称filename的列表集合"""
        for dirpath, dirname, filename in os.walk(filepath_testcase):
            for file in filename:
                # 判断文件以.py结尾且不以__开始,为去除__init.py文件和.pyx后缀的文件
                if file.endswith(".py") and not file.startswith("__"):
                    # print(file)
                    case_list.append(file)
        """3-3.使用unittest.defaultTestLoader.discover()方法,根据路径和case名称寻找对应的case信息"""
        for case in case_list:
            discover = unittest.defaultTestLoader.discover(start_dir=filepath_testcase,
                                                           pattern=case,
                                                           top_level_dir=filepath_testcase)
            """3-4.将找到的case添加至测试套件中"""
            testsuite.addTest(discover)

        """4.执行所有测试套件testsuite中所有测试用例,生成报告,并将报告发送至指定邮箱地址"""
        self.run_and_sendemail(testsuite, pathinfo_dict['reportpath'])

    def runall_bycaselist(self, caselist):
        """
        通过根据传入的case名称列表进行所有case的执行
        1.传入多个case名称即为执行列表中所有接口case
        2.传入单个case名称即执行case下所有接口case
        :param caselist:
        :return:
        """

        """1.根据当前文件路径获得case执行时的路径、报告名称信息"""
        pathinfo_dict = TestData().get_pathinfo_dict(__file__)

        """2.初始化测试套件"""
        testsuite = unittest.TestSuite()

        """3.遍历caselist中的case名称,获取对应接口case,并添加进testsuite"""
        for case in caselist:
            global discover
            """
                3-1.使用unittest.defaultTestLoader.discover()方法,根据路径和case名称寻找对应的case信息;
                filepathdict['filepath_testcase']对应为filepath_testcase
                case对应的文件名称
            """
            discover = unittest.defaultTestLoader.discover(start_dir=pathinfo_dict['filepath_testcase'],
                                                           pattern=case + '.py',
                                                           top_level_dir=None)
            """3-2.将找到的case添加至测试套件中"""
            testsuite.addTest(discover)

        """4.执行所有测试套件testsuite中所有测试用例,生成报告,并将报告发送至指定邮箱地址"""
        self.run_and_sendemail(testsuite, pathinfo_dict['reportpath'])

2. 生成测试报告

本文中,我们在caserunner_myhtml.py文件将用例的执行、测试报告的生成、以及邮件的发送进行了封装。
可在testresult文件目录下查看测试报告和日志文件信息。

testresult文件目录

       到目前为止我们其实已经实现了目标,但是以上操作都是在本地环境进行执行的,接下来我们还需要考虑如何让该工具被所有人使用,才能实现其最大的价值。

3. 使用接口进行调用

一般我们实现了一个测试工具,往往不能只考虑是提供给测试人员使用(或者说只是编写测试用例的同学才会使用);
我们希望开发人员或者其他相关人员都能使用,但是他们又无需在本地部署代码;
所以我们需要考虑如何将测试工具变得容易操作,比如只需调用接口就能执行相关用例;
这个就是我们为什么使用Flask进行项目搭建的原因:
我们使用Flask进行实现了简单的restful风格的接口服务,在内部调用我们的用例执行文件caserunner_myhtml.py,从而达到接口调用的效果。

通常我们Flask项目的入口放在默认的app.py文件中,这个文件名称不是固定的,可以进行自定义以便不同服务应用接口的区分。

aap文件
@app.route('/runallcase', methods=['GET'])
def runallcase():
    """
    执行所有测试用例接口
    :return:
    """

    """调用CaseRunner中的runall()方法"""
    CaseRunner().runall()

    return 'Success'


@app.route('/runallcase_bycaselist', methods=['POST'])
def runallcase_bycaselist():
    """
    根据传入的接口名称执行所有测试用例接口
    :return:
    """

    """1. 获取接口请求post的请求报文信息,并处理为json格式"""
    request_json = request.json
    """2. 获取请求参数中的caselist参数信息"""
    caselist = request_json['caselist']
    """3. 调用CaseRunner中的runall_bycaselist()方法"""
    CaseRunner().runall_bycaselist(caselist)

    return 'Success'

postman请求效果

使用postman进行GET请求

使用postman进行POST请求

五、最终实现效果

通过以上一顿操作后,我们来看看最终实现的效果图

1. 测试报告邮件中的内容

邮件内容包括测试结果的预览信息,附件中报告了生成的*.html*.log文件

测报告邮件信息

2. 生成的html测试报告内容

附件中的*.html文件为整个测试用例执行后的结果信息,包含了测试人员、开始时间、执行耗时等信息

生成的html测报告

还可以通过点击详细按钮查看错误详情

用例验证失败详细信息

3. 生成的log日志文件内容

记录的log日志文件

总结

       以上就是我们使用PythonFlask框架搭配Unittest实现的一个接口自动化测试框架,我将在接下来的章节中,针对该项目中涉及到的一些类库使用做相应的介绍。

占坑

支线文章

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

推荐阅读更多精彩内容