selenium搭建数据驱动的测试框架(附源码)

声明:本文着重讲框架,不讲解具体的python语法。阅读前最好先了解python语言、selenium和unitest的基础。代码里的内容在代码中标明里注释,没有在解释的部分说太多。

本文的测试框架基于python3+selenium+webdriver+unittest的,用于web网页的自动化(适用于PC+H5页面),主要解决以下几个问题:
1.定位器可配置(涉及PageElementLocator.ini,ParseConfigurationFile.py,VarConfig.py)
2.封装页面(涉及LoginPage.py)
3.数据分离(涉及ParseExcel.py,VarConfig.py)
4.业务逻辑封装(涉及LoginAction.py)
5.日志记录(涉及Logger.conf,Log.py)
6.生成测试报告(涉及HTMLTestRunner.py,RunTest.py)

框架的目录结构如下(pycharm里工程目录结构):


image.png

接下来我们通过“登陆”这个功能来串一下整个框架,登陆页如图所示:


image.png

1.定位器配置文件:PageElementLocator.ini文件
使用selenium能够定位到的方式,定位这个页面所有的元素(这里我采用的是xpath,不了解xpath的移步我的另外一篇文章《精简xpath定位总结》),并保存在config包中的PageElementLocator.ini文件里。这个登陆页有三个元素:用户名输入框、密码输入框和登陆按钮,如下代码所示:

[login]
loginPage.username=xpath>//*[@id="username"]
loginPage.password=xpath>//*[@id="password"]
loginPage.loginButton=xpath>//*[@id="loginBtn"]

以上的式子的表达式中,=号前面是该定位表达式对应某个页面类的某个定位方法;>号前是定位方式(xpath),后面是具体的定位表达式。页面类见下一步

2.封装页面类:LoginPage.py文件
这个文件在pageObjects包中,它通过解析在1步骤中的定位器文件,可以获得元素的定位方式和定位表达式,从而封装了几个方法并可以返回对应的element,以提供给后续的元素操作做准备,主要分为解析定位器文件和定位元素两个部分,代码如下:

from util.ObjectMap import *
from util.ParseConfigurationFile import ParseConfigFile
from util.Log import *

class LoginPage:

    def __init__(self,driver):
        self.driver = driver
        #第一部分:解析定位器文件
        self.parseCF=ParseConfigFile()
        self.loginOptions=self.parseCF.getItemsSection('login')

    def userNameObj(self):
        try:
            #根据解析定位文件获得定位方式和定位表达式
            locateType,locateExpression = self.loginOptions['loginPage.username'.lower()].split('>')
            print(locateType,locateExpression)
            #第二部分:根据获得的定位方式和定位表达式定位元素
            element = getElement(self.driver,locateType,locateExpression)
        except Exception as e:
            logger.error(e)
        else:
            logger.info("找到元素"+locateExpression)
            return element

    def passwordObj(self):
        try:
            locateType,locateExpression = self.loginOptions['loginPage.password'.lower()].split('>')
            element = getElement(self.driver,locateType,locateExpression)
        except Exception as e:
            logger.error(e)
        else:
            logger.info("找到元素" + locateExpression)
            return element

    def loginButton(self):
        try:
            locateType,locateExpression = self.loginOptions['loginPage.loginButton'.lower()].split('>')
            element = getElement(self.driver,locateType,locateExpression)
        except Exception as e:
            logger.error(e)
        else:
            logger.info("找到元素" + locateExpression)
            return element

if __name__=="__main__":
    from selenium import webdriver
    import time
    from util.SimulateLogin import simulator_login

    browser = webdriver.Chrome()

    browser.get("https://plogin.m.jd.com/user/login.action")

    login=LoginPage(browser)
    time.sleep(2)
    login.userNameObj().send_keys("13180314708")
    login.passwordObj().send_keys("liujinhong1995")
    login.loginButton().click()
    time.sleep(3)
    simulator_login(browser)
    browser.quit()

第1部分解析定位文件。我们用到了几个其他的文件,第一个是util包中的ParseConfigurationFile.py:

from configparser import ConfigParser
from config.VarConfig import pageElementLocatorPath

class ParseConfigFile:
    def __init__(self):
        self.cf=ConfigParser()
        self.cf.read(pageElementLocatorPath)

    #items方法获取的结果里把字符都转成小写了
    def getItemsSection(self,sectionName):
        optionsDict=self.cf.items(sectionName)
        return dict(optionsDict)

    def getOptionValue(self,sectionName,optionName):
        value=self.cf.get(sectionName,optionName)
        return dict(value)

if __name__=="__main__":
    pc=ParseConfigFile()
    print(pc.getItemsSection('login'))
    print(pc.getOptionValue('login','loginPage.username'))

这个文件的使用了第三方包ConfigParser,该包可以把PageElementLocator.ini文件里的的定位表达式转换成字典,key是=号前面的部分,value是=后面的部分。sectionName为PageElementLocator.ini里中括号内的部分([login]),以区分不同页面的元素。ParseConfigurationFile.py里from config.VarConfig import pageElementLocatorPath,这句是获取到定位器文件的路径,config包中的模块VarConfig.py内容如下:

#coding=utf-8
import os
#获取当前文件所在目录的父目录的绝对路径
parentDirPath=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
#获取存放页面元素定位表达式文件的绝对路径
pageElementLocatorPath=parentDirPath+r"/config/PageElementLocator.ini"
#获取数据文件存放的绝对路径
dataFilePath=parentDirPath+r"/testData/登陆账号.xlsx"

#登陆账号.xlsx中每列对应的数字序号
acount_username=2
acount_password=3
acount_isExecute=4
acount_type=5
acount_comment=6
execute_testResult=7
execute_time=8

if __name__=="__main__":
    print(pageElementLocatorPath)
    print(dataFilePath)

第2部分定位元素。 这句代码 element = getElement(self.driver,locateType,locateExpression)
,getElement方法来源于util包中的ObjectMap模块,它的三个参数分别是:driver、定位方式和定位表达式。该方法封装了webdriver的WebDriverWait方法(这里不再详细展开,不了解的可以百度下),用于更方便的定位一个元素,ObjectMap.py的代码如下:

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def getElement(driver,locateType,locateExpression):
    try:
        element=WebDriverWait(driver,30).until(lambda x:x.find_element(by=locateType,value=locateExpression))
        return element
    except Exception as e:
        print(e)

def getElements(driver,locateType,locateExpression):
    try:
        elements = WebDriverWait(driver,30).until(lambda x:x.find_elements(by=locateType,value=locateExpression))
        return elements
    except Exception as e:
        print(e)

if __name__=='__main__':
    from selenium import webdriver
    driver = webdriver.Chrome()
    driver.get("http://www.baidu.com")
    #driver.find_element("id","kw").send_keys("selenium")
    searchBox=getElement(driver,"id","kw")
    print(searchBox.tag_name)
    elements = getElements(driver,"tag name","a")
    print(len(elements))
    driver.quit()

定位方式和定位表达式来源1部分中的解析结果。

3.数据分离
这里的数据指的是测试脚本里需要用到的数据,比如要测试登陆,就要有正确的用户名和密码,错误的用户名和密码等等。除此之外,还要把测试脚本的执行结果作为数据保存起来。我们采用excel文件的方式来保存测试数据和执行结果,该文件存于testData包中登陆账号.xlsx,如图所示:

image.png

这里我们采用了第三方包openpyxl来做excel的操作,自己封装类一个ParseExcel类,使得调用更为方便,该类存于util包的ParseExcel.py文件中,实现了指定行号和列号取数据,指定列号取数据,向指定的单元格写入数据等方法,代码如下:

from openpyxl import Workbook
from openpyxl import load_workbook
from openpyxl.styles import colors
from openpyxl.styles import Font
import locale
import time

class ParseExcel(object):

    def __init__(self, excel_file_path):
        self.excel_file_path = excel_file_path
        self.wb = load_workbook(excel_file_path)
        self.ws = self.wb[self.wb.sheetnames[0]]
        # print(self.ws.title)

    def get_all_sheet_names(self):
        return self.wb.sheetnames

    def get_sheet_name_by_index(self, index):
        return self.wb.sheetnames[index - 1]

    def get_excel_file_path(self):
        return self.excel_file_path

    def create_sheet(self, sheet_name, position=None):

        try:
            if position:
                self.wb.create_sheet(sheet_name, position)
            else:
                self.wb.create_sheet(sheet_name)
            self.save()
            return True
        except Exception as e:
            print(e)
            return False
    def get_sheet_by_name(self,sheet_name):
        return self.wb.get_sheet_by_name(sheet_name)

    def set_sheet_by_name(self, sheet_name):
        if sheet_name not in self.wb.sheetnames:
            print("%s sheet不存在,请重新设置!" % sheet_name)
            return False
        self.ws = self.wb[sheet_name]
        return True

    def set_sheet_by_index(self, index):
        self.ws = self.wb[self.get_sheet_name_by_index(index)]
        print("设定的sheet名称是:", self.ws.title)

    def get_cell_value(self, row_no, col_no, sheet_name=None):
        if sheet_name is not None:  # 参数设置了新的sheet
            result = self.set_sheet_by_name(sheet_name)
            if result == False:
                return None
        return self.ws.cell(row_no, col_no).value

    def get_row_values(self, row_no, sheet_name=None):
        cell_values = []
        if sheet_name is not None:  # 参数设置了新的sheet
            result = self.set_sheet_by_name(sheet_name)
            if result == False:
                return None

        for cell in list(self.ws.rows)[row_no - 1]:
            cell_values.append(cell.value)

        return cell_values

    def get_col_values(self, col_no, sheet_name=None):
        cell_values = []
        if sheet_name is not None:  # 参数设置了新的sheet
            result = self.set_sheet_by_name(sheet_name)
            if result == False:
                return None
        for cell in list(self.ws.columns)[col_no - 1]:
            cell_values.append(cell.value)
        return cell_values

    def get_some_values(self, min_row_no, min_col_no,max_row_no, max_col_no, sheet_name=None):
        if sheet_name is not None:  # 参数设置了新的sheet
            result = self.set_sheet_by_name(sheet_name)
            if result == False:
                return None
        values = []
        for i in range(min_row_no, max_row_no + 1):
            row_values = []
            for j in range(min_col_no, max_col_no + 1):
                row_values.append(self.ws.cell(row=i, column=j).value)
            values.append(row_values)

        return values

    def save(self):
        self.wb.save(self.excel_file_path)

    def write_cell_value(self, row_no, col_no, value, style=None, sheet_name=None):
        if sheet_name is not None:  # 参数设置了新的sheet
            result = self.set_sheet_by_name(sheet_name)
            if result == False:
                return False
        if style is None:
            style = colors.BLACK
        elif style == "red":
            style = colors.RED
        elif style == "green":
            style = colors.DARKGREEN
        self.ws.cell(row=row_no, column=col_no).font = Font(color=style)
        self.ws.cell(row=row_no, column=col_no, value=value)
        self.save()
        return True

    def write_current_time(self, row_no, col_no, style=None, sheet_name=None):
        if sheet_name is not None:  # 参数设置了新的sheet
            result = self.set_sheet_by_name(sheet_name)
            if result == False:
                return False
        if style is None:
            style = colors.BLACK
        elif style == "red":
            style = colors.RED
        elif style == "greed":
            style = colors.GREEN
        locale.setlocale(locale.LC_ALL, 'en')
        locale.setlocale(locale.LC_CTYPE, 'chinese')
        self.ws.cell(row=row_no, column=col_no).font = Font(color=style)
        self.ws.cell(row=row_no, column=col_no,
                     value=time.strftime("%Y年%m月%d日 %H时%M分%S秒"))
        self.save()
        return True


if __name__ == "__main__":
    excel = ParseExcel(r"D:\study\光荣之路\正式课\第十九天\test.xlsx")
    # print(excel.get_excel_file_path())
    # print(excel.get_cell_value(1,1))
    # print(excel.get_cell_value(3,3))
    # excel.set_sheet_by_name("xxxx")
    # excel.set_sheet_by_name("Sheet2")
    # print(excel.get_cell_value(3,3))
    # print(excel.get_cell_value(3,3,"xxx"))
    # print(excel.get_cell_value(3,3,"Sheet2"))
    # print(excel.get_row_values(1))
    # print(excel.get_row_values(1,"Sheet2"))
    # print(excel.get_col_values(1))
    # print(excel.get_col_values(1,"Sheet2"))
    # print(excel.get_some_values(1,1,5,5))
    # print(excel.get_some_values(1,1,3,3,"Sheet2"))
    # print(excel.write_cell_value(6,1,"光荣之路吴老师","red"))
    # print(excel.write_current_time(6,1,"red"))
    # print(excel.get_all_sheet_names())
    # print(excel.get_sheet_name_by_index(1))
    # excel.set_sheet_by_index(2)
    print(excel.create_sheet("光荣之路"))

另外还在config包的VarConfig.py模块中做了数据文件的路径指定和文件中列号和数据字段的对应关系,上面有给出这个文件,这里我们用到了以下这几行:

#获取数据文件存放的绝对路径
dataFilePath=parentDirPath+r"/testData/登陆账号.xlsx"

#登陆账号.xlsx中每列对应的数字序号
acount_username=2
acount_password=3
acount_isExecute=4
acount_type=5
acount_comment=6
execute_testResult=7
execute_time=8

有了上面的基础,我们就可以在测试脚本中使用excel中的数据了,包testScripts中testLogin.py代码如下,具体代码不再做描述,代码中的注释已经说明:

#coding=utf-8
import time
import unittest
from selenium import webdriver
from appModules.LoginAction import LoginAction
from pageObjects.MinePage import MinePage
from util.ParseExcel import ParseExcel
from config.VarConfig import *
from util.Log import *

class TestLogin(unittest.TestCase):

#1.获取到文件存储路径 dataFilePath,并生成一个excelObj对象,用于操作excel
 excelObj = ParseExcel(dataFilePath)
    def setUp(self) -> None:
        pass

    def tearDown(self) -> None:
        pass

    def test_Login(self):
        logger.info("开始执行登录脚本...")
        #2.获取是否执行列,acount_isExecute来源于导入的模块from config.VarConfig import *
        #acount_isExecute=4,isExecuteUser是一个存储来所有行是否执行的列表。是Y则执行,否则不执行
        isExecuteUser=TestLogin.excelObj.get_col_values(acount_isExecute)
        #遍历每行数据
        for idx,i in enumerate(isExecuteUser[1:]):
            start_time=time.time()
            if i=='Y':
                #获取指定单元格的数据
                username=TestLogin.excelObj.get_cell_value(idx+2,acount_username)
                password=TestLogin.excelObj.get_cell_value(idx+2,acount_password)
                usertype=TestLogin.excelObj.get_cell_value(idx+2,acount_type)
                logger.info("执行测试数据:%s,%s,%s"%(username,password,usertype))
                try:
                    browser = webdriver.Chrome()
                    browser.get('http://test-jdread.jd.com/h5/m/p_my_details')
                    logger.info('启动浏览器,访问"我的"页面...')
                    minePage = MinePage(browser)
                    minePage.LoginEntryButton().click()
                    logger.info('点击"我的"页面的登录按钮...')
                    LoginAction.login(username, password, browser)
                    logger.info('登录操作执行...')

                    try:
                        minePage.ExitButtonObj()  # 如果在"我的"页面找到退出按钮,则通过测试用例,如果没找到该按钮则测试用例未通过
                        logger.info('在"我的"页面找【退出】按钮')
                    except Exception as e:
                        self.assertTrue(1 == 2)
                        logger.debug('在"我的"页面找到【退出】按钮,失败,用例不通过')
                        #失败时:写入执行结果和执行时间
                        TestLogin.excelObj.write_cell_value(idx+2,execute_testResult,'fail','red')
                        TestLogin.excelObj.write_cell_value(idx + 2, execute_time,str(time.time()-start_time)+'ms', 'red')

                    else:
                        self.assertTrue(1 == 1)
                        logger.debug('在"我的"页面找到【退出】按钮,成功,用例通过')
                        #成功时:写入执行结果和执行时间
                        TestLogin.excelObj.write_cell_value(idx + 2, execute_testResult, 'success', 'green')
                        TestLogin.excelObj.write_cell_value(idx + 2, execute_time, str(round((time.time() - start_time)/1000,2)) + 's')

                except Exception as e:
                    logger.error(e)
                    raise e
            else:
                continue

if __name__=="__main__":
    # unittest.main()
    #通过多个测试集合组成一个测试套
    testsuit =  unittest.TestSuite()
    testsuit.addTest(TestLogin("test_Login"))
    #运行测试套,verbosity=2说明输出每个测试用例运行的详细信息
    unittest.TextTestRunner(verbosity=2).run(testsuit)

MinePage.py代码如下:

#coding=utf-8
from util.ObjectMap import *
from util.ParseConfigurationFile import ParseConfigFile
from util.Log import *


class MinePage:

    def __init__(self,driver):
        self.driver = driver
        self.parseCF = ParseConfigFile()
        self.mineOptions = self.parseCF.getItemsSection('mine')

    #登陆状态
    # minePage.exitButton = xpath > // *[text() = "退出登录"]
    # minePage.loginEntryButton = xpath > // label[contains(text(), "点击登录")]
    def ExitButtonObj(self):
        try:
            locateType,locateExpression = self.mineOptions['minePage.exitButton'.lower()].split('>')
            element = getElement(self.driver,locateType,locateExpression)
        except Exception as e:
            logger.error(e)
        else:
            logger.info("找到元素" + locateExpression)
            return element
    #未登陆状态
    def LoginEntryButton(self):
        try:
            locateType,locateExpression = self.mineOptions['minePage.loginEntryButton'.lower()].split('>')
            element = getElement(self.driver,locateType,locateExpression)
        except Exception as e:
            logger.error(e)
        else:
            logger.info("找到元素" + locateExpression)
            return element
    def title(self):
        try:
            locateType,locateExpression = self.mineOptions['minePage.title'.lower()].split('>')
            element = getElement(self.driver,locateType,locateExpression)
        except Exception as e:
            logger.error(e)
        else:
            logger.info("找到元素" + locateExpression)
            return element
    def exitDialogConfirm(self):
        try:
            locateType,locateExpression = self.mineOptions['minePage.exitDialogConfirm'.lower()].split('>')
            element = getElement(self.driver,locateType,locateExpression)
        except Exception as e:
            logger.error(e)
        else:
            logger.info("找到元素" + locateExpression)
            return element

if __name__=="__main__":
    from selenium import webdriver
    import time

    browser = webdriver.Chrome()

    #测试登陆入口按钮
    browser.get('http://test-jdread.jd.com/h5/m/p_my_details')
    minePage=MinePage(browser)
    minePage.LoginEntryButton().click()

    time.sleep(2)
    browser.quit()

4.业务逻辑封装
对于某些业务逻辑非常复杂的脚本,需要我们把一些公共的模块抽象出来,以减少重复代码,提高代码的服用行,这些模块我们放到appMoudules包中,命名方式为xxxAction.py.这里我们通过登陆来举例,整个登陆需要三个元素,一个用户名输入框,一个密码输入框和一个登陆按钮,登陆逻辑就是把这三个元素的获取,点击按钮操作和登陆后跳转到指定页面,都封装到一个方法login里,如果什么地方用到登陆,可以直接调用这个方法。LoginAction.py具体代码如下:

#coding=utf-8
from pageObjects.LoginPage import LoginPage
from util.SimulateLogin import *
from util.Log import *

class LoginAction:
    def __init__(self):
        logger.info("login..")

    @staticmethod
    def login(username,password,browser,source_url=None):
        try:
            # browser.get("https://plogin.m.jd.com/user/login.action")
            #使用了封装的页面类LoginPage
            page = LoginPage(browser)
            page.userNameObj().send_keys(username)
            page.passwordObj().send_keys(password)
            page.loginButton().click()
            time.sleep(3)

            while (1):
                verify_code(browser)
                try:
                    # 这个条件不同情况下调用需要修改
                    element = browser.find_element_by_xpath('//*[@id="captcha"]/div[1]')
                except Exception as e:
                    logger.info("登录成功!")
                    if source_url:
                        browser.get(source_url)
                    return
        except Exception as e:
            logger.error(e)
            raise e
if __name__=="__main__":
    from selenium import webdriver
    import time
    browser=webdriver.Chrome()
    browser.get("https://plogin.m.jd.com/user/login.action")
    LoginAction.login('13180314708','liujinhong1995',browser)
    browser.quit()

5.日志记录
这里采用了第三方包logging来记录日志,通过fileConfig的方法加载日志配置,在日志配置中设置不同的日志模版,模版里设置不同的日志等级:DEBUG、INFO、WARNING、ERROR、CRITICAL,设置不同的日志格式,设置日志的输出方式(文件、控制台等)。想详细了解的可以看这篇:https://www.jianshu.com/p/feb86c06c4f4
在util包中的Log.py对配置进行里加载,config包中的Logger.conf是配置文件,如下代码:
Log.py文件:

# -*- encoding:utf-8 -*-
import logging
import logging.config
from config.VarConfig import parentDirPath

logpath=parentDirPath+"/config/Logger.conf"
logging.config.fileConfig(logpath)

# create logger
#不同环境下只需要修改logger_name就可以切换日志的模板
logger_name = "example01"
logger = logging.getLogger(logger_name)

if __name__=="__main__":
    logger.debug('debug message')
    logger.info('info message')
    logger.warning('warn message')
    logger.error('error message')

Logger.conf文件

[loggers]
keys=root,example01,example02

#logger概述
[logger_root]
level=DEBUG
handlers=hand01,hand02,hand03,hand04

#一个日志输出的模板(测试环境)
[logger_example01]
handlers=hand01,hand02
qualname=example01
propagate=0

#一个日志输出的模板(线上环境,不需要输出debug和info)
[logger_example02]
handlers=hand03,hand04
qualname=example02
propagate=0

[handlers]
keys=hand01,hand02,hand03,hand04

[handler_hand01]
class=StreamHandler#把日志输出到控制台,日志级别大于等于INFO时输出
level=INFO
formatter=form01
args=(sys.stderr,)

[handler_hand02]
class=FileHandler#把日志输出到文件里,日志级别大于等于DEBUG时输出
level=DEBUG
formatter=form01
args=('../log/DataDrivenFrameWork_test.log', 'a')

[handler_hand03]
class=StreamHandler#把日志输出到控制台,日志级别大于等于WARNING时输出
level=WARNING
formatter=form01
args=(sys.stderr,)

[handler_hand04]
class=FileHandler#把日志输出到文件里,日志级别大于等于WARNING时输出
level=WARNING
formatter=form01
args=('../log/DataDrivenFrameWork_online.log', 'a')

[formatters]
keys=form01

[formatter_form01]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s
datefmt=%Y-%m-%d-%H:%M:%S

有了以上两个文件的基础,我们就能在测试脚本中需要的地方加log了。此处可以参考3中的testLogin.py代码。
在脚本执行前记录开始

 logger.info("开始执行登录脚本...")

在执行完脚本后记录结果日志:

logger.debug('在"我的"页面找到【退出】按钮,失败,用例不通过')

报错的的时候记录日志:

  logger.error(e)

你可以在任何你觉得需要记录的地方打log,一般会在脚本执行开始、结束和报错的时候记录日志。
6.生成测试报告

对于所有的测试脚本,我们应该有一个统一管理运行的文件,testScripts包中的RunTest.py,在这个文件中,把所有的脚本放到测试套件里面,运行完脚本后统一生成测试报告,这里我们使用里unittest的测试套。使用HTMLTestRunner.py生成测试报告(这个文件不需要仔细阅读,会用即可)。
RunTest.py文件:

#coding=utf-8
import unittest
import os
from util import HTMLTestRunner


if __name__=="__main__":
    # 加载当前目录下所有有效的测试模块(以test开头的py文件),“.”表示当前目录
    testSuite = unittest.TestLoader().discover('.')
    filename = "../test.html"  # 定义个报告存放路径,支持相对路径。
    # 以二进制方式打开文件,准备写
    fp = open(filename, 'wb')
    # 使用HTMLTestRunner配置参数,输出报告路径、报告标题、描述,均可以配
    runner = HTMLTestRunner.HTMLTestRunner(stream=fp,
                                           title='测试报告', description='京东阅读M站自动化测试报告')
    # 运行测试集合
    runner.run(testSuite)

HTMLTestRunner.py文件:

"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the result at a glance.
 
The simplest way to use this is to invoke its main method. E.g.
 
    import unittest
    import HTMLTestRunner
 
    ... define your tests ...
 
    if __name__ == '__main__':
        HTMLTestRunner.main()
 
 
For more customization options, instantiates a HTMLTestRunner object.
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.
 
    # output to a file
    fp = file('my_report.html', 'wb')
    runner = HTMLTestRunner.HTMLTestRunner(
                stream=fp,
                title='My unit test',
                description='This demonstrates the report output by HTMLTestRunner.'
                )
 
    # Use an external stylesheet.
    # See the Template_mixin class for more customizable options
    runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'
 
    # run the test
    runner.run(my_test_suite)
 
 
------------------------------------------------------------------------
Copyright (c) 2004-2007, Wai Yip Tung
All rights reserved.
 
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
 
* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name Wai Yip Tung nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.
 
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
 
# URL: http://tungwaiyip.info/software/HTMLTestRunner.html
 
__author__ = "Wai Yip Tung"
__version__ = "0.8.2"
 
 
"""
Change History
 
Version 0.8.2
* Show output inline instead of popup window (Viorel Lupu).
 
Version in 0.8.1
* Validated XHTML (Wolfgang Borgert).
* Added description of test classes and test cases.
 
Version in 0.8.0
* Define Template_mixin class for customization.
* Workaround a IE 6 bug that it does not treat <script> block as CDATA.
 
Version in 0.7.1
* Back port to Python 2.3 (Frank Horowitz).
* Fix missing scroll bars in detail log (Podi).
"""
 
# TODO: color stderr
# TODO: simplify javascript using ,ore than 1 class in the class attribute?
 
import datetime
import io
import sys
import time
import unittest
from xml.sax import saxutils
 
 
# ------------------------------------------------------------------------
# The redirectors below are used to capture output during testing. Output
# sent to sys.stdout and sys.stderr are automatically captured. However
# in some cases sys.stdout is already cached before HTMLTestRunner is
# invoked (e.g. calling logging.basicConfig). In order to capture those
# output, use the redirectors for the cached stream.
#
# e.g.
#   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
#   >>>
 
class OutputRedirector(object):
    """ Wrapper to redirect stdout or stderr """
    def __init__(self, fp):
        self.fp = fp
 
    def write(self, s):
        self.fp.write(s)
 
    def writelines(self, lines):
        self.fp.writelines(lines)
 
    def flush(self):
        self.fp.flush()
 
stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)
 
 
 
# ----------------------------------------------------------------------
# Template
 
class Template_mixin(object):
    """
    Define a HTML template for report customerization and generation.
 
    Overall structure of an HTML report
 
    HTML
    +------------------------+
    |<html>                  |
    |  <head>                |
    |                        |
    |   STYLESHEET           |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |  </head>               |
    |                        |
    |  <body>                |
    |                        |
    |   HEADING              |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |   REPORT               |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |   ENDING               |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |  </body>               |
    |</html>                 |
    +------------------------+
    """
 
    STATUS = {
    0: 'pass',
    1: 'fail',
    2: 'error',
    }
 
    DEFAULT_TITLE = 'Unit Test Report'
    DEFAULT_DESCRIPTION = ''
 
    # ------------------------------------------------------------------------
    # HTML Template
 
    HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>%(title)s</title>
    <meta name="generator" content="%(generator)s"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    %(stylesheet)s
</head>
<body>
<script language="javascript" type="text/javascript"><!--
output_list = Array();
 
/* level - 0:Summary; 1:Failed; 2:All */
function showCase(level) {
    trs = document.getElementsByTagName("tr");
    for (var i = 0; i < trs.length; i++) {
        tr = trs[i];
        id = tr.id;
        if (id.substr(0,2) == 'ft') {
            if (level < 1) {
                tr.className = 'hiddenRow';
            }
            else {
                tr.className = '';
            }
        }
        if (id.substr(0,2) == 'pt') {
            if (level > 1) {
                tr.className = '';
            }
            else {
                tr.className = 'hiddenRow';
            }
        }
    }
}
 
 
function showClassDetail(cid, count) {
    var id_list = Array(count);
    var toHide = 1;
    for (var i = 0; i < count; i++) {
        tid0 = 't' + cid.substr(1) + '.' + (i+1);
        tid = 'f' + tid0;
        tr = document.getElementById(tid);
        if (!tr) {
            tid = 'p' + tid0;
            tr = document.getElementById(tid);
        }
        id_list[i] = tid;
        if (tr.className) {
            toHide = 0;
        }
    }
    for (var i = 0; i < count; i++) {
        tid = id_list[i];
        if (toHide) {
            document.getElementById('div_'+tid).style.display = 'none'
            document.getElementById(tid).className = 'hiddenRow';
        }
        else {
            document.getElementById(tid).className = '';
        }
    }
}
 
 
function showTestDetail(div_id){
    var details_div = document.getElementById(div_id)
    var displayState = details_div.style.display
    // alert(displayState)
    if (displayState != 'block' ) {
        displayState = 'block'
        details_div.style.display = 'block'
    }
    else {
        details_div.style.display = 'none'
    }
}
 
 
function html_escape(s) {
    s = s.replace(/&/g,'&');
    s = s.replace(/</g,'<');
    s = s.replace(/>/g,'>');
    return s;
}
 
/* obsoleted by detail in <div>
function showOutput(id, name) {
    var w = window.open("", //url
                    name,
                    "resizable,scrollbars,status,width=800,height=450");
    d = w.document;
    d.write("<pre>");
    d.write(html_escape(output_list[id]));
    d.write("\n");
    d.write("<a href='javascript:window.close()'>close</a>\n");
    d.write("</pre>\n");
    d.close();
}
*/
--></script>
 
%(heading)s
%(report)s
%(ending)s
 
</body>
</html>
"""
    # variables: (title, generator, stylesheet, heading, report, ending)
 
 
    # ------------------------------------------------------------------------
    # Stylesheet
    #
    # alternatively use a <link> for external style sheet, e.g.
    #   <link rel="stylesheet" href="$url" type="text/css">
 
    STYLESHEET_TMPL = """
<style type="text/css" media="screen">
body        { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
table       { font-size: 100%; }
pre         { }
 
/* -- heading ---------------------------------------------------------------------- */
h1 {
    font-size: 16pt;
    color: gray;
}
.heading {
    margin-top: 0ex;
    margin-bottom: 1ex;
}
 
.heading .attribute {
    margin-top: 1ex;
    margin-bottom: 0;
}
 
.heading .description {
    margin-top: 4ex;
    margin-bottom: 6ex;
}
 
/* -- css div popup ------------------------------------------------------------------------ */
a.popup_link {
}
 
a.popup_link:hover {
    color: red;
}
 
.popup_window {
    display: none;
    position: relative;
    left: 0px;
    top: 0px;
    /*border: solid #627173 1px; */
    padding: 10px;
    background-color: #E6E6D6;
    font-family: "Lucida Console", "Courier New", Courier, monospace;
    text-align: left;
    font-size: 8pt;
    width: 500px;
}
 
}
/* -- report ------------------------------------------------------------------------ */
#show_detail_line {
    margin-top: 3ex;
    margin-bottom: 1ex;
}
#result_table {
    width: 80%;
    border-collapse: collapse;
    border: 1px solid #777;
}
#header_row {
    font-weight: bold;
    color: white;
    background-color: #777;
}
#result_table td {
    border: 1px solid #777;
    padding: 2px;
}
#total_row  { font-weight: bold; }
.passClass  { background-color: #6c6; }
.failClass  { background-color: #c60; }
.errorClass { background-color: #c00; }
.passCase   { color: #6c6; }
.failCase   { color: #c60; font-weight: bold; }
.errorCase  { color: #c00; font-weight: bold; }
.hiddenRow  { display: none; }
.testcase   { margin-left: 2em; }
 
 
/* -- ending ---------------------------------------------------------------------- */
#ending {
}
 
</style>
"""
 
 
 
    # ------------------------------------------------------------------------
    # Heading
    #
 
    HEADING_TMPL = """<div class='heading'>
<h1>%(title)s</h1>
%(parameters)s
<p class='description'>%(description)s</p>
</div>
 
""" # variables: (title, parameters, description)
 
    HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
""" # variables: (name, value)
 
 
 
    # ------------------------------------------------------------------------
    # Report
    #
 
    REPORT_TMPL = """
<p id='show_detail_line'>Show
<a href='javascript:showCase(0)'>Summary</a>
<a href='javascript:showCase(1)'>Failed</a>
<a href='javascript:showCase(2)'>All</a>
</p>
<table id='result_table'>
<colgroup>
<col align='left' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
</colgroup>
<tr id='header_row'>
    <td>Test Group/Test case</td>
    <td>Count</td>
    <td>Pass</td>
    <td>Fail</td>
    <td>Error</td>
    <td>View</td>
</tr>
%(test_list)s
<tr id='total_row'>
    <td>Total</td>
    <td>%(count)s</td>
    <td>%(Pass)s</td>
    <td>%(fail)s</td>
    <td>%(error)s</td>
    <td> </td>
</tr>
</table>
""" # variables: (test_list, count, Pass, fail, error)
 
    REPORT_CLASS_TMPL = r"""
<tr class='%(style)s'>
    <td>%(desc)s</td>
    <td>%(count)s</td>
    <td>%(Pass)s</td>
    <td>%(fail)s</td>
    <td>%(error)s</td>
    <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
</tr>
""" # variables: (style, desc, count, Pass, fail, error, cid)
 
 
    REPORT_TEST_WITH_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    <td colspan='5' align='center'>
 
    <!--css div popup start-->
    <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
        %(status)s</a>
 
    <div id='div_%(tid)s' class="popup_window">
        <div style='text-align: right; color:red;cursor:pointer'>
        <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
           [x]</a>
        </div>
        <pre>
        %(script)s
        </pre>
    </div>
    <!--css div popup end-->
 
    </td>
</tr>
""" # variables: (tid, Class, style, desc, status)
 
 
    REPORT_TEST_NO_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    <td colspan='5' align='center'>%(status)s</td>
</tr>
""" # variables: (tid, Class, style, desc, status)
 
 
    REPORT_TEST_OUTPUT_TMPL = r"""
%(id)s: %(output)s
""" # variables: (id, output)
 
 
 
    # ------------------------------------------------------------------------
    # ENDING
    #
 
    ENDING_TMPL = """<div id='ending'> </div>"""
 
# -------------------- The end of the Template class -------------------
 
 
TestResult = unittest.TestResult
 
class _TestResult(TestResult):
    # note: _TestResult is a pure representation of results.
    # It lacks the output and reporting ability compares to unittest._TextTestResult.
 
    def __init__(self, verbosity=1):
        TestResult.__init__(self)
        self.stdout0 = None
        self.stderr0 = None
        self.success_count = 0
        self.failure_count = 0
        self.error_count = 0
        self.verbosity = verbosity
 
        # result is a list of result in 4 tuple
        # (
        #   result code (0: success; 1: fail; 2: error),
        #   TestCase object,
        #   Test output (byte string),
        #   stack trace,
        # )
        self.result = []
 
 
    def startTest(self, test):
        TestResult.startTest(self, test)
        # just one buffer for both stdout and stderr
        self.outputBuffer = io.StringIO()
        stdout_redirector.fp = self.outputBuffer
        stderr_redirector.fp = self.outputBuffer
        self.stdout0 = sys.stdout
        self.stderr0 = sys.stderr
        sys.stdout = stdout_redirector
        sys.stderr = stderr_redirector
 
 
    def complete_output(self):
        """
        Disconnect output redirection and return buffer.
        Safe to call multiple times.
        """
        if self.stdout0:
            sys.stdout = self.stdout0
            sys.stderr = self.stderr0
            self.stdout0 = None
            self.stderr0 = None
        return self.outputBuffer.getvalue()
 
 
    def stopTest(self, test):
        # Usually one of addSuccess, addError or addFailure would have been called.
        # But there are some path in unittest that would bypass this.
        # We must disconnect stdout in stopTest(), which is guaranteed to be called.
        self.complete_output()
 
 
    def addSuccess(self, test):
        self.success_count += 1
        TestResult.addSuccess(self, test)
        output = self.complete_output()
        self.result.append((0, test, output, ''))
        if self.verbosity > 1:
            sys.stderr.write('ok ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('.')
 
    def addError(self, test, err):
        self.error_count += 1
        TestResult.addError(self, test, err)
        _, _exc_str = self.errors[-1]
        output = self.complete_output()
        self.result.append((2, test, output, _exc_str))
        if self.verbosity > 1:
            sys.stderr.write('E  ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('E')
 
    def addFailure(self, test, err):
        self.failure_count += 1
        TestResult.addFailure(self, test, err)
        _, _exc_str = self.failures[-1]
        output = self.complete_output()
        self.result.append((1, test, output, _exc_str))
        if self.verbosity > 1:
            sys.stderr.write('F  ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('F')
 
 
class HTMLTestRunner(Template_mixin):
    """
    """
    def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
        self.stream = stream
        self.verbosity = verbosity
        if title is None:
            self.title = self.DEFAULT_TITLE
        else:
            self.title = title
        if description is None:
            self.description = self.DEFAULT_DESCRIPTION
        else:
            self.description = description
 
        self.startTime = datetime.datetime.now()
 
 
    def run(self, test):
        "Run the given test case or test suite."
        result = _TestResult(self.verbosity)
        test(result)
        self.stopTime = datetime.datetime.now()
        self.generateReport(test, result)
        # print >> sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime)
        print(sys.stderr, '\nTime Elapsed: %s' % (self.stopTime-self.startTime))
        return result
 
 
    def sortResult(self, result_list):
        # unittest does not seems to run in any particular order.
        # Here at least we want to group them together by class.
        rmap = {}
        classes = []
        for n,t,o,e in result_list:
            cls = t.__class__
            if not cls in rmap:
                rmap[cls] = []
                classes.append(cls)
            rmap[cls].append((n,t,o,e))
        r = [(cls, rmap[cls]) for cls in classes]
        return r
 
 
    def getReportAttributes(self, result):
        """
        Return report attributes as a list of (name, value).
        Override this to add custom attributes.
        """
        startTime = str(self.startTime)[:19]
        duration = str(self.stopTime - self.startTime)
        status = []
        if result.success_count: status.append('Pass %s'    % result.success_count)
        if result.failure_count: status.append('Failure %s' % result.failure_count)
        if result.error_count:   status.append('Error %s'   % result.error_count  )
        if status:
            status = ' '.join(status)
        else:
            status = 'none'
        return [
            ('Start Time', startTime),
            ('Duration', duration),
            ('Status', status),
        ]
 
 
    def generateReport(self, test, result):
        report_attrs = self.getReportAttributes(result)
        generator = 'HTMLTestRunner %s' % __version__
        stylesheet = self._generate_stylesheet()
        heading = self._generate_heading(report_attrs)
        report = self._generate_report(result)
        ending = self._generate_ending()
        output = self.HTML_TMPL % dict(
            title = saxutils.escape(self.title),
            generator = generator,
            stylesheet = stylesheet,
            heading = heading,
            report = report,
            ending = ending,
        )
        self.stream.write(output.encode('utf8'))
 
 
    def _generate_stylesheet(self):
        return self.STYLESHEET_TMPL
 
 
    def _generate_heading(self, report_attrs):
        a_lines = []
        for name, value in report_attrs:
            line = self.HEADING_ATTRIBUTE_TMPL % dict(
                    name = saxutils.escape(name),
                    value = saxutils.escape(value),
                )
            a_lines.append(line)
        heading = self.HEADING_TMPL % dict(
            title = saxutils.escape(self.title),
            parameters = ''.join(a_lines),
            description = saxutils.escape(self.description),
        )
        return heading
 
 
    def _generate_report(self, result):
        rows = []
        sortedResult = self.sortResult(result.result)
        for cid, (cls, cls_results) in enumerate(sortedResult):
            # subtotal for a class
            np = nf = ne = 0
            for n,t,o,e in cls_results:
                if n == 0: np += 1
                elif n == 1: nf += 1
                else: ne += 1
 
            # format class description
            if cls.__module__ == "__main__":
                name = cls.__name__
            else:
                name = "%s.%s" % (cls.__module__, cls.__name__)
            doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
            desc = doc and '%s: %s' % (name, doc) or name
 
            row = self.REPORT_CLASS_TMPL % dict(
                style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
                desc = desc,
                count = np+nf+ne,
                Pass = np,
                fail = nf,
                error = ne,
                cid = 'c%s' % (cid+1),
            )
            rows.append(row)
 
            for tid, (n,t,o,e) in enumerate(cls_results):
                self._generate_report_test(rows, cid, tid, n, t, o, e)
 
        report = self.REPORT_TMPL % dict(
            test_list = ''.join(rows),
            count = str(result.success_count+result.failure_count+result.error_count),
            Pass = str(result.success_count),
            fail = str(result.failure_count),
            error = str(result.error_count),
        )
        return report
 
 
    def _generate_report_test(self, rows, cid, tid, n, t, o, e):
        # e.g. 'pt1.1', 'ft1.1', etc
        has_output = bool(o or e)
        tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
        name = t.id().split('.')[-1]
        doc = t.shortDescription() or ""
        desc = doc and ('%s: %s' % (name, doc)) or name
        tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL
 
        # o and e should be byte string because they are collected from stdout and stderr?
        if isinstance(o,str):
            # TODO: some problem with 'string_escape': it escape \n and mess up formating
            # uo = unicode(o.encode('string_escape'))
            # uo = o.decode('latin-1')
            uo = e
        else:
            uo = o
        if isinstance(e,str):
            # TODO: some problem with 'string_escape': it escape \n and mess up formating
            # ue = unicode(e.encode('string_escape'))
            # ue = e.decode('latin-1')
            ue = e
        else:
            ue = e
 
        script = self.REPORT_TEST_OUTPUT_TMPL % dict(
            id = tid,
            output = saxutils.escape(str(uo)+ue),
        )
 
        row = tmpl % dict(
            tid = tid,
            Class = (n == 0 and 'hiddenRow' or 'none'),
            style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
            desc = desc,
            script = script,
            status = self.STATUS[n],
        )
        rows.append(row)
        if not has_output:
            return
 
    def _generate_ending(self):
        return self.ENDING_TMPL
 
 
##############################################################################
# Facilities for running tests from the command line
##############################################################################
 
# Note: Reuse unittest.TestProgram to launch test. In the future we may
# build our own launcher to support more specific command line
# parameters like test title, CSS, etc.
class TestProgram(unittest.TestProgram):
    """
    A variation of the unittest.TestProgram. Please refer to the base
    class for command line parameters.
    """
    def runTests(self):
        # Pick HTMLTestRunner as the default test runner.
        # base class's testRunner parameter is not useful because it means
        # we have to instantiate HTMLTestRunner before we know self.verbosity.
        if self.testRunner is None:
            self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
        unittest.TestProgram.runTests(self)
 
main = TestProgram
 
##############################################################################
# Executing this module from the command line
##############################################################################
 
if __name__ == "__main__":
    main(module=None)

下面给出整个工程的完整目录,以做参考:


image.png

至此,这个框架就完成了,如果大家有任何问题可以在评论里说,欢迎大家讨论。
看到最后就是有福利,嫌麻烦的小伙伴移步git:https://github.com/yaqingirl/

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

推荐阅读更多精彩内容