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 = ''
 
    # 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 
 
%(heading)s
%(report)s
%(ending)s
 


"""
    # variables: (title, generator, stylesheet, heading, report, ending)
 
 
    # ------------------------------------------------------------------------
    # Stylesheet
    #
    # alternatively use a  for external style sheet, e.g.
    #   
 
    STYLESHEET_TMPL = """

"""
 
 
 
    # ------------------------------------------------------------------------
    # Heading
    #
 
    HEADING_TMPL = """

%(title)s

%(parameters)s

%(description)s

""" # variables: (title, parameters, description) HEADING_ATTRIBUTE_TMPL = """

%(name)s: %(value)s

""" # variables: (name, value) # ------------------------------------------------------------------------ # Report # REPORT_TMPL = """

Show Summary Failed All

%(test_list)s
Test Group/Test case Count Pass Fail Error View
Total %(count)s %(Pass)s %(fail)s %(error)s
""" # variables: (test_list, count, Pass, fail, error) REPORT_CLASS_TMPL = r""" %(desc)s %(count)s %(Pass)s %(fail)s %(error)s Detail """ # variables: (style, desc, count, Pass, fail, error, cid) REPORT_TEST_WITH_OUTPUT_TMPL = r"""
%(desc)s
%(status)s """ # variables: (tid, Class, style, desc, status) REPORT_TEST_NO_OUTPUT_TMPL = r"""
%(desc)s
%(status)s """ # variables: (tid, Class, style, desc, status) REPORT_TEST_OUTPUT_TMPL = r""" %(id)s: %(output)s """ # variables: (id, output) # ------------------------------------------------------------------------ # ENDING # ENDING_TMPL = """
""" # -------------------- 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/

你可能感兴趣的:(selenium搭建数据驱动的测试框架(附源码))