美文网首页
selenium搭建数据驱动的测试框架(附源码)

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

作者: Judy警官 | 来源:发表于2019-06-05 21:48 被阅读0次

    声明:本文着重讲框架,不讲解具体的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/

    相关文章

      网友评论

          本文标题:selenium搭建数据驱动的测试框架(附源码)

          本文链接:https://www.haomeiwen.com/subject/eytexctx.html