├── framework ├── __init__.py ├── README.md ├── case_strategy.py ├── ReadConfig.py ├── browser_engine.py ├── logger.py ├── ConnectDataBase.py ├── base_page.py └── HTMLTestReportCN.py ├── logs ├── README.md └── 2019-06-12 │ └── 2019-06-12_01_35_06.log ├── pageobjects ├── __init__.py ├── README.md └── BaiDu │ ├── __pycache__ │ ├── Index.cpython-36.pyc │ └── news.cpython-36.pyc │ ├── news.py │ └── Index.py ├── screenshots └── README.md ├── tools └── README.md ├── test_report ├── README.md └── 百度自动化测试报告2019-06-12 │ └── 百度自动化测试报告2019-06-12_01_35_06 │ └── 自动化测试报告.html ├── config ├── README.md └── config.ini ├── requirements.txt ├── test_suites ├── README.md └── test_baidu │ ├── __pycache__ │ ├── __init__.cpython-36.pyc │ ├── test_01_index.cpython-36.pyc │ └── test_02_news.cpython-36.pyc │ ├── __init__.py │ ├── test_02_news.py │ └── test_01_index.py ├── TestRunner.py └── README.md /framework/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logs/README.md: -------------------------------------------------------------------------------- 1 | 输出的日志文件 -------------------------------------------------------------------------------- /pageobjects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pageobjects/README.md: -------------------------------------------------------------------------------- 1 | 页面对象 -------------------------------------------------------------------------------- /screenshots/README.md: -------------------------------------------------------------------------------- 1 | 截图保存文件夹 -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | driver驱动文件夹 -------------------------------------------------------------------------------- /test_report/README.md: -------------------------------------------------------------------------------- 1 | HTML测试报告输出文件夹 -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | 浏览器,url的基础配置,及一些基本不需要更改的账号信息 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium==3.141.0 2 | PyMySQL==0.9.3 3 | -------------------------------------------------------------------------------- /test_suites/README.md: -------------------------------------------------------------------------------- 1 | test_suites测试用例总集合,总集合可以包括多个子集合 2 | 3 | test_xxxx为测试用例子集合,子集合可以包括多个用例 4 | 5 | TestRunner.py运行符合规则的所有测试用例 -------------------------------------------------------------------------------- /pageobjects/BaiDu/__pycache__/Index.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RongXiaoCong/WebUI-Automation-Frame/HEAD/pageobjects/BaiDu/__pycache__/Index.cpython-36.pyc -------------------------------------------------------------------------------- /pageobjects/BaiDu/__pycache__/news.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RongXiaoCong/WebUI-Automation-Frame/HEAD/pageobjects/BaiDu/__pycache__/news.cpython-36.pyc -------------------------------------------------------------------------------- /framework/README.md: -------------------------------------------------------------------------------- 1 | 页面基础类 base_page: 封装一些常用的页面操作方法 2 | 3 | 浏览器引擎类 browser_engine: 封装浏览器驱动选择,浏览器初始化 4 | 5 | 日志类 Logger: 封装日志输出及控制台输出方法 6 | 7 | 数据库类 ConnectDataBase: 封装了数据库的简单操作 8 | 9 | -------------------------------------------------------------------------------- /test_suites/test_baidu/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RongXiaoCong/WebUI-Automation-Frame/HEAD/test_suites/test_baidu/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /test_suites/test_baidu/__pycache__/test_01_index.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RongXiaoCong/WebUI-Automation-Frame/HEAD/test_suites/test_baidu/__pycache__/test_01_index.cpython-36.pyc -------------------------------------------------------------------------------- /test_suites/test_baidu/__pycache__/test_02_news.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RongXiaoCong/WebUI-Automation-Frame/HEAD/test_suites/test_baidu/__pycache__/test_02_news.cpython-36.pyc -------------------------------------------------------------------------------- /config/config.ini: -------------------------------------------------------------------------------- 1 | [BROWSERTYPE] 2 | ;browserName = Firefox 3 | browserName = Chrome 4 | #browserName = IE 5 | 6 | [BROWSERATTRIBUTE] 7 | implicitly_wait = 2 8 | 9 | [TESTSERVER] 10 | url = http://www.baidu.com/ 11 | 12 | [TESTACCOUNT] 13 | account = haha 14 | password = 123456 15 | name = 测试大佬 16 | 17 | [TESTDATA] 18 | 19 | 20 | [DATABASE] 21 | host = '' 22 | user = '' 23 | password = '' 24 | database = '' 25 | port = '' 26 | 27 | -------------------------------------------------------------------------------- /pageobjects/BaiDu/news.py: -------------------------------------------------------------------------------- 1 | from framework.base_page import BasePage 2 | import unittest 3 | from selenium.webdriver.common.by import By 4 | 5 | class NewsPage(BasePage): 6 | 7 | def __init__(self, driver): 8 | super().__init__(driver) 9 | self.driver = driver 10 | 11 | ''' 12 | **********************************************百度新闻首页************************************** 13 | ''' 14 | 15 | self.i_search = (By.ID, 'ww') # 搜索框 16 | self.b_search = (By.ID, 's_btn_wr') # 搜索按钮 17 | self.b_scroll_news = (By.ID, 'imgView') # 首页的滚动新闻 18 | self.b_scroll_news_title = (By.ID, 'imgTitle') # 首页的滚动新闻的标题 19 | # self.b_news_all_tab = (By.XPATH, '//*[@id="channel-all"]//*[@class="lavalamp-item"]/a') # 新闻首页的导航条所有按钮 20 | 21 | 22 | # 其他页面元素 23 | self.b_news = (By.LINK_TEXT, '新闻') # 新闻按钮 -------------------------------------------------------------------------------- /TestRunner.py: -------------------------------------------------------------------------------- 1 | #_*_ coding:utf-8 _*_ 2 | 3 | import unittest 4 | import sys 5 | import os 6 | sys.path.append(os.path.abspath(os.path.dirname(__file__))) 7 | from framework import HTMLTestReportCN 8 | from framework.ReadConfig import ReadConfig 9 | from framework.case_strategy import CaseStrategy 10 | from test_suites.test_baidu import to_init 11 | 12 | class RunAllTests(object): 13 | 14 | def __init__(self): 15 | cs = CaseStrategy() 16 | self.test_suite = cs.collect_cases() 17 | self.tester = ReadConfig().get_test_account()['name'] 18 | 19 | def run(self): 20 | to_init().clean_test_data() 21 | 22 | # 启动测试时创建文件夹并获取报告的名字 23 | daf = HTMLTestReportCN.DirAndFiles() 24 | daf.create_dir(title='百度自动化测试报告') 25 | report_path = HTMLTestReportCN.GlobalMsg.get_value("report_path") 26 | 27 | with open(report_path, "wb") as fp: 28 | runner = HTMLTestReportCN.HTMLTestRunner(stream=fp, title='百度自动化测试报告', description='用例执行情况:', tester=self.tester) 29 | runner.run(self.test_suite) 30 | 31 | to_init().clean_test_data() 32 | 33 | if __name__ == "__main__": 34 | RunAllTests().run() 35 | 36 | -------------------------------------------------------------------------------- /test_suites/test_baidu/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | from framework.browser_engine import BrowserEngine 4 | from framework.ReadConfig import ReadConfig 5 | from framework.HTMLTestReportCN import DirAndFiles 6 | from framework.ConnectDataBase import ConnectDataBase 7 | from pageobjects.BaiDu.Index import IndexPage 8 | from pageobjects.BaiDu.news import NewsPage 9 | 10 | 11 | class to_init(): 12 | def __init__(self): 13 | self.user_info = {"userName":"hahaha", "password":"ahahah"} 14 | self.search_info = {"1":"python大法好", "2":"人生苦短,我用python"} 15 | 16 | def get_driver(self): 17 | """打开网页并获取driver""" 18 | browser = BrowserEngine(self) 19 | driver = browser.open_browser(self) 20 | return driver 21 | 22 | def clean_test_data(self): 23 | """清理测试数据""" 24 | pass 25 | 26 | def get_daf(): 27 | """ 28 | 获取测试报告对象 29 | :return: 30 | """ 31 | return DirAndFiles() 32 | 33 | def get_bd(driver): 34 | """ 35 | 获取百度首页 36 | :return: 37 | """ 38 | return IndexPage(driver) 39 | 40 | def get_news(driver): 41 | """ 42 | 获取百度新闻页面 43 | :return: 44 | """ 45 | return NewsPage(driver) -------------------------------------------------------------------------------- /framework/case_strategy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | 5 | class CaseStrategy: 6 | def __init__(self): 7 | self.suite_path = 'test_suites' 8 | self.case_path = 'test_baidu' 9 | self.case_pattern = 'test_0*.py' 10 | 11 | def _collect_cases(self, cases, top_dir=None): 12 | suites = unittest.defaultTestLoader.discover(self.case_path, 13 | pattern=self.case_pattern, top_level_dir=top_dir) 14 | for suite in suites: 15 | for case in suite: 16 | cases.addTest(case) 17 | 18 | def collect_cases(self, suite=True): 19 | """collect cases 20 | 21 | collect cases from the giving path by case_path via the giving pattern by case_pattern 22 | 23 | return: all cases that collected by the giving path and pattern, it is a unittest.TestSuite() 24 | 25 | """ 26 | cases = unittest.TestSuite() 27 | 28 | if suite: 29 | test_suites = [] 30 | project_dir = os.path.dirname(os.path.dirname(__file__)) 31 | for file in os.listdir(project_dir): 32 | if self.suite_path in file: 33 | suites_dir = os.path.join(project_dir,file) 34 | if os.path.isdir(suites_dir): 35 | test_suites.append(suites_dir) 36 | for test_suite in test_suites: 37 | self._collect_cases(cases, top_dir=test_suite) 38 | else: 39 | self._collect_cases(cases, top_dir=None) 40 | 41 | return cases 42 | -------------------------------------------------------------------------------- /framework/ReadConfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from configparser import ConfigParser 4 | import os 5 | import sys 6 | # 配置文件路径 7 | configPath = os.path.join(os.path.dirname(os.path.dirname(__file__)) , 'config/config.ini') 8 | 9 | class ReadConfig: 10 | def __init__(self): 11 | self.cf = ConfigParser() 12 | self.cf.read(configPath, encoding='UTF-8') 13 | 14 | def get_browser_type(self): 15 | browserName = self.cf.get("BROWSERTYPE", "browserName") 16 | return browserName 17 | 18 | def get_browser_attribute(self): 19 | implicitly_wait = self.cf.get("BROWSERATTRIBUTE", "implicitly_wait") 20 | return implicitly_wait 21 | 22 | def get_test_server(self): 23 | url = self.cf.get("TESTSERVER", "url") 24 | return url 25 | 26 | def get_test_account(self): 27 | account = self.cf.get("TESTACCOUNT", "account") 28 | password = self.cf.get("TESTACCOUNT", "password") 29 | name = self.cf.get("TESTACCOUNT", "name") 30 | value ={'account':account,'password':password,'name':name} 31 | return value 32 | 33 | def get_database(self): 34 | user = self.cf.get("DATABASE", "user") 35 | password = self.cf.get("DATABASE", "password") 36 | database = self.cf.get("DATABASE", "database") 37 | host = self.cf.get("DATABASE", "host") 38 | port = self.cf.get("DATABASE", "port") 39 | value = {'user':user,'password':password,'database':database,'host':host,'port':port} 40 | return value 41 | 42 | def get_test_data(self, *args): 43 | """ 44 | 从配置文件中获取测试数据,接收可变数量的参数 45 | :param args: 46 | :return: 返回字典 47 | """ 48 | a={} 49 | for i in args: 50 | a[i]=self.cf.get("TESTDATA", i) 51 | return a 52 | -------------------------------------------------------------------------------- /pageobjects/BaiDu/Index.py: -------------------------------------------------------------------------------- 1 | from framework.base_page import BasePage 2 | import unittest 3 | from selenium.webdriver.common.by import By 4 | 5 | class IndexPage(BasePage): 6 | 7 | def __init__(self, driver): 8 | super().__init__(driver) 9 | self.driver = driver 10 | 11 | ''' 12 | **********************************************百度首页************************************** 13 | ''' 14 | 15 | self.i_search = (By.ID, 'kw') # 搜索框 16 | self.b_search = (By.ID, 'su') # 搜索按钮 17 | self.b_news = (By.NAME, 'tj_trnews') # 新闻按钮 18 | self.b_hao123 = (By.NAME, 'tj_trhao123') # hao123按钮 19 | self.b_map = (By.NAME, 'tj_trmap') # 地图按钮 20 | self.b_video = (By.NAME, 'tj_trvideo') # 视频按钮 21 | self.b_tieba = (By.NAME, 'tj_trtieba') # 贴吧按钮 22 | self.b_xueshu = (By.LINK_TEXT, '学术') # 学术按钮 23 | self.b_login = (By.LINK_TEXT, '登录') # 登录按钮 24 | self.b_setting = (By.LINK_TEXT, '设置') # 设置按钮 25 | self.b_more_prod = (By.LINK_TEXT, '更多产品') # 更多产品 26 | 27 | self.b_login_user_name = (By.XPATH, '//*[text()="用户名登录"]') # 用户名登录按钮 28 | self.i_login_user_name = (By.CLASS_NAME, 'pass-text-input-userName') # 用户名输入框 29 | self.i_login_password = (By.CLASS_NAME, 'pass-text-input-password') # 密码输入框 30 | self.b_login_submit = (By.ID, 'TANGRAM__PSP_10__submit') # 用户名密码登录页的登录按钮 31 | 32 | self.b_result_title = (By.XPATH, '//*[@id = "content_left"]//*[@class = "t"]') #搜索结果标题 33 | # 其他页面内容 34 | self.t_search_result = (By.CLASS_NAME, 'nums_text') # 搜索结果 35 | self.t_hot_news = (By.XPATH, '//*[text()="热点要闻"]') # 新闻页面醒目的“热点要闻”四个字 36 | -------------------------------------------------------------------------------- /framework/browser_engine.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import os.path 4 | from framework.ReadConfig import ReadConfig 5 | from selenium import webdriver 6 | from framework.logger import Logger 7 | 8 | logger = Logger(logger="BrowserEngine").getlog() 9 | 10 | 11 | class BrowserEngine(object): 12 | 13 | def __init__(self, driver): 14 | self.driver = driver 15 | self.dir = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) # 注意相对路径获取方法 16 | # windows的驱动程序后缀有exe 17 | # self.chrome_driver_path = self.dir + '/tools/chromedriver.exe' 18 | # mac的驱动程序后缀无exe 19 | self.chrome_driver_path = self.dir + '/tools/chromedriver' 20 | 21 | def open_browser(self, driver): 22 | # 读取配置 23 | config = ReadConfig() 24 | browser = config.get_browser_type() 25 | url = config.get_test_server() 26 | implicitly_wait = config.get_browser_attribute() 27 | 28 | if browser == "Firefox": 29 | driver = webdriver.Firefox() 30 | logger.info("Starting firefox browser.") 31 | elif browser == "Chrome": 32 | driver = webdriver.Chrome(self.chrome_driver_path) 33 | logger.info("Starting Chrome browser.") 34 | elif browser == "IE": 35 | driver = webdriver.Ie() 36 | logger.info("Starting IE browser.") 37 | 38 | driver.get(url) 39 | logger.info("Open url: %s" % url) 40 | # driver.maximize_window() 41 | logger.info("Maximize the current window.") 42 | driver.implicitly_wait(implicitly_wait) 43 | logger.info("Set implicitly wait %s seconds."%str(implicitly_wait)) 44 | return driver 45 | 46 | def quit_browser(self): 47 | logger.info("Now, Close and quit the browser.") 48 | self.driver.quit() -------------------------------------------------------------------------------- /test_suites/test_baidu/test_02_news.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import time 3 | import unittest 4 | from framework.logger import Logger 5 | from selenium.webdriver.common.by import By 6 | from selenium.common.exceptions import NoSuchElementException 7 | from framework.ConnectDataBase import ConnectDataBase 8 | from framework.ReadConfig import ReadConfig 9 | from test_suites.test_baidu import * 10 | 11 | logger = Logger(logger="Baidu > News").getlog() 12 | 13 | class News(unittest.TestCase): 14 | """新闻页测试""" 15 | @classmethod 16 | def setUpClass(cls): 17 | """ 18 | 测试固件的setUp()的代码,主要是测试的前提准备工作 19 | :return: 20 | """ 21 | ini = to_init() 22 | cls.driver = ini.get_driver() 23 | cls.daf = get_daf() 24 | cls.news = get_news(cls.driver) 25 | 26 | @classmethod 27 | def tearDownClass(cls): 28 | """ 29 | 测试结束后的操作,这里基本上都是关闭浏览器 30 | :return: 31 | """ 32 | cls.driver.quit() 33 | 34 | def setUp(self): 35 | pass 36 | 37 | def test_01_check_scroll_news(self): 38 | """检查滚动新闻是否正常跳转""" 39 | self.news.wait_text(self.news.b_news, '新闻') 40 | self.news.click(self.news.b_news) 41 | self.news.wait_gone(self.news.b_news) 42 | # title = self.news.title 43 | origin_handle = self.driver.current_window_handle 44 | # 切换句柄,断言网页标题是否包含滚动新闻的标题 45 | for handle in self.driver.window_handles: 46 | if handle != origin_handle: 47 | self.driver.switch_to_window(handle) 48 | 49 | self.news.wait((By.XPATH, '//*[text()="国内"]'), displayed=False) 50 | 51 | self.news.click(self.news.b_scroll_news) 52 | self.news.sleep(3) 53 | # 这里关闭的是百度新闻首页,而非最后打开的新闻详情页,因为当前页面句柄对应百度新闻首页 54 | self.news.close() 55 | # 这里又将句柄切换回百度首页(当前页面的新建/关闭页面不会修改当前句柄,修改当前句柄会切换当前页面) 56 | self.driver.switch_to.window(origin_handle) 57 | self.news.sleep(3) 58 | logger.info(self.news.get_current_function() + ' --> Successed') -------------------------------------------------------------------------------- /test_suites/test_baidu/test_01_index.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import time 3 | import unittest 4 | from framework.logger import Logger 5 | from selenium.webdriver.common.by import By 6 | from selenium.common.exceptions import NoSuchElementException 7 | from framework.ConnectDataBase import ConnectDataBase 8 | from framework.ReadConfig import ReadConfig 9 | from test_suites.test_baidu import * 10 | 11 | logger = Logger(logger="Baidu > Index").getlog() 12 | 13 | class Index(unittest.TestCase): 14 | """首页测试""" 15 | @classmethod 16 | def setUpClass(cls): 17 | """ 18 | 测试固件的setUp()的代码,主要是测试的前提准备工作 19 | :return: 20 | """ 21 | ini = to_init() 22 | cls.driver = ini.get_driver() 23 | cls.daf = get_daf() 24 | cls.bd = get_bd(cls.driver) 25 | cls.user_info = ini.user_info 26 | cls.search_info = ini.search_info 27 | 28 | @classmethod 29 | def tearDownClass(cls): 30 | """ 31 | 测试结束后的操作,这里基本上都是关闭浏览器 32 | :return: 33 | """ 34 | cls.driver.quit() 35 | 36 | def setUp(self): 37 | pass 38 | 39 | def test_01_search(self): 40 | """搜索""" 41 | self.bd.send_keys(self.bd.i_search, self.search_info['1']) 42 | self.bd.click(self.bd.b_search) 43 | # 验证是否成功搜索到搜索结果的元素 44 | self.assertTrue(self.bd.wait(self.bd.t_search_result)) 45 | print(self.bd.get_current_function()) 46 | logger.info(self.bd.get_current_function() + ' --> Successed') 47 | 48 | def test_02_search_retry(self): 49 | """重新搜索""" 50 | title_1 = [element.text for element in self.bd.find_element(self.bd.b_result_title, None, displayed=False)] 51 | logger.info(title_1) 52 | self.bd.send_keys(self.bd.i_search, self.search_info['2']) 53 | self.bd.click(self.bd.b_search) 54 | self.bd.wait_text_gone(self.bd.b_result_title, title_1, None, displayed=False) 55 | title_2 = [element.text for element in self.bd.find_element(self.bd.b_result_title, None, displayed=False)] 56 | logger.info(title_2) 57 | logger.info(self.bd.get_current_function() + ' --> Successed') 58 | 59 | -------------------------------------------------------------------------------- /framework/logger.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | import logging 3 | import os 4 | import time 5 | 6 | 7 | class Logger(object): 8 | def __init__(self, logger): 9 | ''''' 10 | 指定保存日志的文件路径,日志级别,以及调用文件 11 | 将日志存入到指定的文件中 12 | ''' 13 | 14 | self.path = os.path.join(os.path.dirname(os.path.dirname(__file__)),'logs') 15 | 16 | now_2d = time.strftime("%Y-%m-%d", time.localtime(time.time())) 17 | now_2s = time.strftime("%Y-%m-%d_%H_%M_%S", time.localtime(time.time())) 18 | dir_path = os.path.join(self.path , now_2d) 19 | # 判断文件夹是否存在,不存在则创建 20 | if os.path.isdir(dir_path): 21 | pass 22 | else: 23 | os.makedirs(dir_path) 24 | 25 | # 创建一个logger 26 | self.logger = logging.getLogger(logger) 27 | self.logger.setLevel(logging.DEBUG) 28 | 29 | # 由于需要多次创建logger对象,所以有较低概率会产生1个以上的日志文件,此处进行一次修正 30 | try: 31 | last_log_name = os.listdir(dir_path)[-1] 32 | last_log_timestamp = time.mktime(time.strptime(last_log_name[:last_log_name.find('.')], "%Y-%m-%d_%H_%M_%S")) 33 | now_log_timestamp = time.mktime(time.strptime(now_2s, "%Y-%m-%d_%H_%M_%S")) 34 | if now_log_timestamp - last_log_timestamp < 2: 35 | # 创建一个handler,用于写入日志文件 36 | log_name = dir_path + '/' + last_log_name 37 | else: 38 | log_name = dir_path + '/' + now_2s + '.log' 39 | except IndexError: 40 | log_name = dir_path + '/' + now_2s + '.log' 41 | 42 | fh = logging.FileHandler(log_name,encoding='utf-8') 43 | fh.setLevel(logging.INFO) 44 | 45 | # 再创建一个handler,用于输出到控制台 46 | ch = logging.StreamHandler() 47 | ch.setLevel(logging.INFO) 48 | 49 | # 定义handler的输出格式 50 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 51 | fh.setFormatter(formatter) 52 | ch.setFormatter(formatter) 53 | 54 | # 给logger添加handler 55 | self.logger.addHandler(fh) 56 | self.logger.addHandler(ch) 57 | 58 | def getlog(self): 59 | return self.logger 60 | 61 | -------------------------------------------------------------------------------- /framework/ConnectDataBase.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | from framework.logger import Logger 3 | import sys 4 | from framework.ReadConfig import ReadConfig 5 | 6 | # create a logger instance 7 | logger = Logger(logger="DataBase").getlog() 8 | 9 | class ConnectDataBase: 10 | # 构造函数 11 | def __init__(self): 12 | db_dict = ReadConfig().get_database() 13 | self.host = db_dict['host'] 14 | self.user = db_dict['user'] 15 | self.pwd = db_dict['password'] 16 | self.db = db_dict['database'] 17 | self.port = int(db_dict['port']) 18 | self.conn = None 19 | self.cur = None 20 | 21 | # 连接数据库 22 | def connect(self): 23 | try: 24 | self.conn = pymysql.connect(self.host, self.user, 25 | self.pwd, port=self.port, charset='utf8') 26 | logger.info("成功连接数据库!") 27 | except Exception as e: 28 | logger.error(e) 29 | return False 30 | self.cur = self.conn.cursor() 31 | return True 32 | 33 | # 关闭数据库 34 | def close(self): 35 | # 如果数据打开,则关闭;否则没有操作 36 | if self.conn and self.cur: 37 | self.cur.close() 38 | self.conn.close() 39 | logger.info("成功结束数据库连接!") 40 | return True 41 | 42 | # 执行数据库的sq语句,主要用来做插入操作 43 | def execute(self, sql, params=None): 44 | try: 45 | if self.conn and self.cur: 46 | # 正常逻辑,执行sql,提交操作 47 | self.cur.execute(sql, params) 48 | self.conn.commit() 49 | logger.info("execute sql successed : >>>{}<<< \nparams : >>>{}<<<".format(sql,params)) 50 | except Exception as e: 51 | logger.error("execute sql failed : >>>{}<<< \nparams : >>>{}<<<".format(sql,params)) 52 | logger.error(e) 53 | return False 54 | return True 55 | 56 | # 用来查询多条数据 57 | def fetchall(self, sql, params=None): 58 | self.execute(sql, params) 59 | return self.cur.fetchall() 60 | 61 | # 用来查询单条数据 62 | def fetchone(self, sql, params=None): 63 | self.execute(sql, params) 64 | return self.cur.fetchone() 65 | 66 | 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web UI 自动化测试框架(基于selenium) 2 | ## 框架简介 ## 3 | 基于python语言对selnium做的二次封装,主要有以下特点: 4 | 1.采用了主流的po模式 5 | 2.实现了日志的记录与输出 6 | 3.美观的测试报告输出 7 | 4.灵活的测试用例获取 8 | 5.数据库连接 9 | 6.基础信息配置 10 | 7.整合了大部分常用的方法进行了封装,尤其如find_element,click,send_keys等方法进行了高度封装,实现了智能等待 11 | 8.代码不冗余,复用率高 12 | 13 | ## 适合人群 ## 14 | 1.web自动化小白:帮助其迅速了解web自动化,并上手初步的自动化项目。 15 | 2.测试老司机:节省自己编写框架的时间,直接可用,省时省力,老少皆宜。 16 | 17 | ## 如何开始 ## 18 | 1.安装必要的环境,诸如python3+,selenium 19 | 2.下载合适的浏览器驱动,并放置在/tools文件夹下 20 | [Chrome](http://chromedriver.storage.googleapis.com/index.html) [Firefox](https://github.com/mozilla/geckodriver/releases) [Edge](https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/) 21 | 3.使用终端进入项目根目录,运行以下命令 22 | python TestRunner.py 23 | 24 | ## 框架目录构造: ## 25 | 26 | - **[config](https://github.com/RongXiaoCong/WebUI-Automation-Frame/tree/master/config):** 27 | - *[config.ini](https://github.com/RongXiaoCong/WebUI-Automation-Frame/blob/master/config/config.ini)*:浏览器,url的基础配置,及一些基本不需要更改的账号信息 28 | 29 | - **[framwork](https://github.com/RongXiaoCong/WebUI-Automation-Frame/tree/master/framework):** 30 | - *[logger.py](https://github.com/RongXiaoCong/WebUI-Automation-Frame/blob/master/framework/logger.py)*:封装了日志输入,包括文件输出和控制台的输出 31 | - *[base_page](https://github.com/RongXiaoCong/WebUI-Automation-Frame/blob/master/framework/base_page.py)*:封装了selenium库中常用的方法,包括定位,点击,输入,是否存在,等待等,是耗费了最多心力的类,本项目精华所在... 32 | - *[browser_engine](https://github.com/RongXiaoCong/WebUI-Automation-Frame/blob/master/framework/browser_engine.py)*:通过读取配置文件去选择浏览器和url,并返回浏览器对象实例 33 | - *[ConnectDataBase](https://github.com/RongXiaoCong/WebUI-Automation-Frame/blob/master/framework/ConnectDataBase.py)*:封装了数据库的简单操作 34 | - *[case_strategy](https://github.com/RongXiaoCong/WebUI-Automation-Frame/blob/master/framework/case_strategy.py)*:封装了用例采集策略 35 | 36 | - **[logs](https://github.com/RongXiaoCong/WebUI-Automation-Frame/tree/master/logs):** 37 | 接收日志文件的输出 38 | 39 | - **[pageobjects](https://github.com/RongXiaoCong/WebUI-Automation-Frame/tree/master/pageobjects):** 40 | 用于封装页面对象 41 | 42 | - **[test_report](https://github.com/RongXiaoCong/WebUI-Automation-Frame/tree/master/test_report):** 43 | 测试报告的输出文件夹 44 | 45 | - **[testsuites](https://github.com/RongXiaoCong/WebUI-Automation-Frame/tree/master/test_suites):** 46 | 用于测试用例的存放和测试用例集合 47 | 48 | - **[tools](https://github.com/RongXiaoCong/WebUI-Automation-Frame/tree/master/tools):** 49 | 存放浏览器驱动 50 | 51 | 52 | 53 | 本项目最初参考了https://github.com/StrawberryFlavor/Selenium-Framework 的目录结构,有兴趣小伙伴的可以移步 54 | 55 | 有任何建议和疑问请提issue,佛系回复。 56 | -------------------------------------------------------------------------------- /logs/2019-06-12/2019-06-12_01_35_06.log: -------------------------------------------------------------------------------- 1 | 2019-06-12 01:35:08,124 - BrowserEngine - INFO - Starting Chrome browser. 2 | 2019-06-12 01:35:12,535 - BrowserEngine - INFO - Open url: http://www.baidu.com/ 3 | 2019-06-12 01:35:12,535 - BrowserEngine - INFO - Maximize the current window. 4 | 2019-06-12 01:35:12,538 - BrowserEngine - INFO - Set implicitly wait 2 seconds. 5 | 2019-06-12 01:35:12,588 - ElementOperation - INFO - Positioned the element ('id', 'kw')[0]. 6 | 2019-06-12 01:35:12,795 - ElementOperation - INFO - SendKeys --> ('id', 'kw')[0] success 7 | 2019-06-12 01:35:12,818 - ElementOperation - INFO - Positioned the element ('id', 'su')[0]. 8 | 2019-06-12 01:35:12,869 - ElementOperation - INFO - Click --> ('id', 'su')[0] success 9 | 2019-06-12 01:35:13,861 - ElementOperation - INFO - Positioned the element ('class name', 'nums_text')[0]. 10 | 2019-06-12 01:35:13,925 - Baidu > Index - INFO - test_01_search --> Successed 11 | 2019-06-12 01:35:14,077 - ElementOperation - INFO - Positioned the elements ('xpath', '//*[@id = "content_left"]//*[@class = "t"]'). 12 | 2019-06-12 01:35:14,216 - Baidu > Index - INFO - ['Python大法好 - 微信公众号:虾神daxialu(咨询问题请加) - CSDN博客', '专栏:Python大法好 - CSDN博客', 'Python大法好啊 - 简书', 'python大法好——面向对象 - null? - 博客园', 'python大法好,这些你可不要忽略了! - 云+社区 - 腾讯云', '如何理解python大法好_百度知道', 'python大法好——网络编程 - null? - 博客园', 'Python大法好', '如何理解python大法好?_百度知道'] 13 | 2019-06-12 01:35:14,233 - ElementOperation - INFO - Positioned the element ('id', 'kw')[0]. 14 | 2019-06-12 01:35:14,360 - ElementOperation - INFO - SendKeys --> ('id', 'kw')[0] success 15 | 2019-06-12 01:35:14,376 - ElementOperation - INFO - Positioned the element ('id', 'su')[0]. 16 | 2019-06-12 01:35:14,407 - ElementOperation - INFO - Click --> ('id', 'su')[0] success 17 | 2019-06-12 01:35:14,493 - ElementOperation - INFO - Positioned the elements ('xpath', '//*[@id = "content_left"]//*[@class = "t"]'). 18 | 2019-06-12 01:35:14,569 - ElementOperation - INFO - The text of the target element is "['Python大法好 - 微信公众号:虾神daxialu(咨询问题请加) - CSDN博客', '专栏:Python大法好 - CSDN博客', 'Python大法好啊 - 简书', 'python大法好——面向对象 - null? - 博客园', 'python大法好,这些你可不要忽略了! - 云+社区 - 腾讯云', '如何理解python大法好_百度知道', 'python大法好——网络编程 - null? - 博客园', 'Python大法好', '如何理解python大法好?_百度知道']",the actual text is "['Python大法好 - 微信公众号:虾神daxialu(咨询问题请加) - CSDN博客', '专栏:Python大法好 - CSDN博客', 'Python大法好啊 - 简书', 'python大法好——面向对象 - null? - 博客园', 'python大法好,这些你可不要忽略了! - 云+社区 - 腾讯云', '如何理解python大法好_百度知道', 'python大法好——网络编程 - null? - 博客园', 'Python大法好', '如何理解python大法好?_百度知道']"... 19 | Start matching>>>>>> 20 | 2019-06-12 01:35:14,569 - ElementOperation - INFO - Sleep for 0.5 seconds 21 | 2019-06-12 01:35:15,176 - ElementOperation - INFO - Positioned the elements ('xpath', '//*[@id = "content_left"]//*[@class = "t"]'). 22 | 2019-06-12 01:35:15,318 - ElementOperation - INFO - The text of the target element is "['Python大法好 - 微信公众号:虾神daxialu(咨询问题请加) - CSDN博客', '专栏:Python大法好 - CSDN博客', 'Python大法好啊 - 简书', 'python大法好——面向对象 - null? - 博客园', 'python大法好,这些你可不要忽略了! - 云+社区 - 腾讯云', '如何理解python大法好_百度知道', 'python大法好——网络编程 - null? - 博客园', 'Python大法好', '如何理解python大法好?_百度知道']",the actual text is "['人生苦短,我用Python?为什么Python这么火? - Python达人 - CSDN博客', 'Python崛起:“人生苦短,我用Python”并非一句戏言 - qq..._CSDN博客', '为何说人生苦短我用Python', '人生苦短,我用Python(目录) - 海燕。 - 博客园', '为什么说”人生苦短,我用python“? - IT。拾荒者 - 博客园', '人生苦短,我用Python或Java', '人生苦短,我用Python', '人生苦短,我用python! - 简书', '“人生苦短,我用Python” - 简书']"... 23 | Start matching>>>>>> 24 | 2019-06-12 01:35:15,318 - ElementOperation - INFO - Matching success! 25 | 2019-06-12 01:35:15,393 - ElementOperation - INFO - Positioned the elements ('xpath', '//*[@id = "content_left"]//*[@class = "t"]'). 26 | 2019-06-12 01:35:15,469 - Baidu > Index - INFO - ['人生苦短,我用Python?为什么Python这么火? - Python达人 - CSDN博客', 'Python崛起:“人生苦短,我用Python”并非一句戏言 - qq..._CSDN博客', '为何说人生苦短我用Python', '人生苦短,我用Python(目录) - 海燕。 - 博客园', '为什么说”人生苦短,我用python“? - IT。拾荒者 - 博客园', '人生苦短,我用Python或Java', '人生苦短,我用Python', '人生苦短,我用python! - 简书', '“人生苦短,我用Python” - 简书'] 27 | 2019-06-12 01:35:15,472 - Baidu > Index - INFO - test_02_search_retry --> Successed 28 | 2019-06-12 01:35:20,900 - BrowserEngine - INFO - Starting Chrome browser. 29 | 2019-06-12 01:35:24,877 - BrowserEngine - INFO - Open url: http://www.baidu.com/ 30 | 2019-06-12 01:35:24,878 - BrowserEngine - INFO - Maximize the current window. 31 | 2019-06-12 01:35:24,879 - BrowserEngine - INFO - Set implicitly wait 2 seconds. 32 | 2019-06-12 01:35:24,935 - ElementOperation - INFO - Positioned the element ('link text', '新闻')[0]. 33 | 2019-06-12 01:35:24,963 - ElementOperation - INFO - The text of the target element is "新闻",the actual text is "新闻"... 34 | Start matching>>>>>> 35 | 2019-06-12 01:35:24,964 - ElementOperation - INFO - Matching success! 36 | 2019-06-12 01:35:25,023 - ElementOperation - INFO - Positioned the element ('link text', '新闻')[0]. 37 | 2019-06-12 01:35:26,606 - ElementOperation - INFO - Click --> ('link text', '新闻')[0] success 38 | 2019-06-12 01:35:28,672 - ElementOperation - ERROR - Failed to position the element ('link text', '新闻')[0]! 39 | 2019-06-12 01:35:28,693 - ElementOperation - INFO - Positioned the element ('xpath', '//*[text()="国内"]')[0]. 40 | 2019-06-12 01:35:28,719 - ElementOperation - INFO - Positioned the element ('id', 'imgView')[0]. 41 | 2019-06-12 01:35:28,788 - ElementOperation - INFO - Click --> ('id', 'imgView')[0] success 42 | 2019-06-12 01:35:28,818 - ElementOperation - INFO - Closing current window. 43 | 2019-06-12 01:35:28,852 - Baidu > News - INFO - test_01_check_scroll_news --> Successed 44 | -------------------------------------------------------------------------------- /test_report/百度自动化测试报告2019-06-12/百度自动化测试报告2019-06-12_01_35_06/自动化测试报告.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 百度自动化测试报告 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 104 | 105 | 106 | 107 | 373 |
374 |
375 |
376 |

百度自动化测试报告

377 |

测试人员 : 测试大佬

378 |

开始时间 : 2019-06-12 01:35:06

379 |

合计耗时 : 0:00:24.552301

380 |

测试结果 : 共 3,通过 3,通过率 = 100.00%

381 |

失败用例合集 :

382 |

错误用例合集 :

383 | 384 |

用例执行情况:

385 |
386 |
387 |
388 | 389 | 390 | 391 |
392 |

393 | 概要{ 100.00% } 394 | 通过{ 3 } 395 | 失败{ 0 } 396 | 错误{ 0 } 397 | 所有{ 3 } 398 |

399 |
400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 |
用例集/测试用例说明总计通过失败错误耗时详细
Index首页测试22002.93秒详细
test_01_search
搜索 437 | 440 | 441 | 442 | 443 |
444 |
445 |         
446 | pt1_1: test_01_search
447 | 
448 | 
449 |         
450 |
451 |
test_02_search_retry
重新搜索通过
News新闻页测试11003.97秒详细
test_01_check_scroll_news
检查滚动新闻是否正常跳转通过
总计33006.9秒通过率:100.00%
490 | 491 |
 
492 | 495 | 496 | 497 | 498 | 499 | -------------------------------------------------------------------------------- /framework/base_page.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import time 3 | from selenium.common.exceptions import ElementNotInteractableException,WebDriverException 4 | import os.path 5 | from framework.logger import Logger 6 | from framework.HTMLTestReportCN import DirAndFiles 7 | import time 8 | import inspect 9 | from selenium.webdriver.common.by import By 10 | from selenium.webdriver.remote.webdriver import WebDriver 11 | 12 | # create a logger instance 13 | logger = Logger(logger="ElementOperation").getlog() 14 | # last_locate='' 15 | 16 | class BasePage(object): 17 | """ 18 | 定义一个页面基类,让所有页面都继承这个类,封装常用的页面操作方法到这个类 19 | """ 20 | 21 | def __init__(self, driver): 22 | self.driver = driver 23 | self.daf = DirAndFiles() 24 | 25 | @property 26 | def title(self): 27 | """获取当前网页的标题""" 28 | max_times = 5 29 | for i in range(max_times): 30 | try: 31 | return self.driver.title 32 | except WebDriverException as e : 33 | if i < max_times-1: 34 | time.sleep(0.5) 35 | print(1) 36 | else: 37 | raise e 38 | 39 | @property 40 | def browser_name(self): 41 | """获取浏览器名称""" 42 | return self.driver.name 43 | 44 | @property 45 | def page_source(self): 46 | """获取页面所有元素""" 47 | return self.driver.page_source 48 | 49 | @property 50 | def current_window_handle(self): 51 | """获取当前窗口句柄""" 52 | return self.driver.current_window_handle 53 | 54 | @property 55 | def window_handles(self): 56 | """获取当前浏览器窗口句柄""" 57 | return self.driver.window_handles 58 | 59 | @property 60 | def switch_to(self): 61 | """跳转到一个元素、框架、窗口""" 62 | return self.driver.switch_to 63 | 64 | def get(self, url): 65 | """打开一个新页面""" 66 | return self.driver.get(url) 67 | 68 | def refresh(self): 69 | """刷新当前页面""" 70 | return self.driver.refresh() 71 | 72 | def execute_script(self, script, *args): 73 | """执行js命令""" 74 | logger.info("Execute the JS command {} {}.".format(script,*args)) 75 | return self.driver.execute_script(script, *args) 76 | 77 | def current_url(self): 78 | """获取当前url""" 79 | return self.driver.current_url() 80 | 81 | def forward(self): 82 | """浏览器前进""" 83 | logger.info("Click forward on current page.") 84 | return self.driver.forward() 85 | 86 | def back(self): 87 | """浏览器后退""" 88 | logger.info("Click back on current page.") 89 | return self.driver.back() 90 | 91 | def close(self): 92 | """关闭当前窗口""" 93 | logger.info("Closing current window.") 94 | return self.driver.close() 95 | 96 | def set_window_size(self, width, height, windowHandle='current'): 97 | """设置窗口尺寸""" 98 | return self.driver.set_window_size(width, height, windowHandle) 99 | 100 | def get_window_size(self, windowHandle='current'): 101 | """获取窗口尺寸""" 102 | return self.driver.get_window_size(windowHandle) 103 | 104 | def sleep(self, seconds): 105 | """ 106 | 程序休眠 107 | :param seconds: 等待时长(s) 108 | :return: 109 | """ 110 | logger.info("Sleep for {} seconds".format(seconds)) 111 | time.sleep(seconds) 112 | 113 | def get_current_function(self): 114 | """获取当前方法名称""" 115 | return inspect.stack()[1][3] 116 | 117 | def swipe(self, direction, length=None): 118 | """ 119 | 滑动窗口 120 | :param direction: 滑动方向,可以是up,down,left,right 121 | :param length: 滑动的长度,为默认值时滑动半屏 122 | :return: 123 | """ 124 | # 如果长度为默认值,则获取当前屏幕的长宽,设置滑动长度 125 | if length == None and direction == 'up' or direction == 'down': 126 | length = 0.5*float(self.driver.get_window_size()['height']) 127 | elif length == None and direction == 'left' or direction == 'right': 128 | length = 0.5*float(self.driver.get_window_size()['width']) 129 | 130 | if direction == 'up': 131 | logger.info('The screen scrolling up and the length is {}.'.format(length)) 132 | self.driver.execute_script('window.scrollBy(0,{})'.format(length)) 133 | elif direction == 'down': 134 | logger.info('The screen scrolling down and the length is {}.'.format(length)) 135 | length = str(-1*int(length)) 136 | self.driver.execute_script('window.scrollBy(0,{})'.format(length)) 137 | elif direction == 'left': 138 | logger.info('The screen scrolling left and the length is {}.'.format(length)) 139 | self.driver.execute_script('window.scrollBy({},0)'.format(length)) 140 | elif direction == 'right': 141 | logger.info('The screen scrolling right and the length is {}.'.format(length)) 142 | length = str(-1*int(length)) 143 | self.driver.execute_script('window.scrollBy({},0)'.format(length)) 144 | else: 145 | logger.error('Entered the wrong direction "{}", please confirm whether it is one of up, down, left, right!'.format(direction)) 146 | raise WebDriverException('Entered the wrong direction "{}", please confirm whether it is one of up, down, left, right!'.format(direction)) 147 | 148 | def find_element(self, locate, index=0, max_times=20, delay=0.5, displayed=True): 149 | """ 150 | 定位元素方法,用于定位多个属性相同的元素,当index为None时返回的是符合匹配规则的元素列表 151 | :param locate: 元素定位 152 | :param index: 序号,第一个为0,第二个为1,以此类推,默认为0,当为None时返回所有符合条件的元素 153 | :param max_times: 最大循环次数,默认为20次 154 | :param delay: 延时,默认为0.5秒 155 | :param displayed: 是否必须等待到元素可见才算找到元素,默认为是 156 | :return: 157 | """ 158 | # 是否报错的标志 159 | error = False 160 | # 是否已定位到的标志 161 | flag = False 162 | for i in range(max_times): 163 | # 当前已定位到num个元素 164 | num = 0 165 | try: 166 | elements = self.driver.find_elements(*locate) 167 | if elements == None or elements == []: 168 | if i < max_times-1: 169 | logger.info("The server doesn't send a response and the position will be retried after {} seconds!".format(delay)) 170 | continue 171 | if index == None: 172 | for element in elements: 173 | if element.is_displayed() == False and displayed == True: 174 | if i < max_times-1: 175 | logger.info("The elements has been positioned but one of the elements is not visible and will be repositioned after {} seconds!".format(delay)) 176 | break 177 | else: 178 | num += 1 179 | if num == len(elements): 180 | flag = True 181 | logger.info("Positioned the elements {}.".format(locate)) 182 | return elements 183 | else: 184 | element = elements[index] 185 | if element.is_displayed() == False and displayed == True: 186 | logger.info("The element has been positioned but the element is not visible and will be repositioned after {} seconds!".format(delay)) 187 | else: 188 | flag = True 189 | logger.info("Positioned the element {}[{}].".format(locate, index)) 190 | return element 191 | except WebDriverException as e: 192 | if i == max_times - 1: 193 | error = True 194 | logger.error(e) 195 | raise WebDriverException(e.msg, self.daf.get_screenshot(self.driver)) 196 | if index == None: 197 | logger.info("The elements {} are not successfully positioned and will be retried after hibernation.".format(locate)) 198 | else: 199 | logger.info("The element {}[{}] is not successfully positioned and will be retried after hibernation.".format(locate, index)) 200 | finally: 201 | if i < max_times-1 and flag != True: 202 | self.sleep(delay) 203 | elif i == max_times-1 and error != True: 204 | if index == None: 205 | logger.error("Failed to position the elements {}!".format(locate)) 206 | raise WebDriverException("Failed to position the elements {}!".format(locate)) 207 | else: 208 | logger.error("Failed to position the element {}[{}]!".format(locate, index)) 209 | raise WebDriverException("Failed to position the element {}[{}]!".format(locate, index)) 210 | 211 | def wait(self, locate, index=0, max_times=20, delay=0.5, displayed=True): 212 | """ 213 | 等待元素出现 214 | :param locate: 元素定位 215 | :param index: 序号,第一个为0,第二个为1,以此类推,默认为0 216 | :param max_times: 最大循环次数,默认为20次 217 | :param delay: 延时,默认为0.5秒 218 | :param displayed: 是否必须等待到元素可见才算找到元素,默认为是 219 | :return: 220 | """ 221 | try: 222 | self.find_element(locate, index, max_times, delay, displayed=displayed) 223 | return True 224 | except WebDriverException as e: 225 | if 'Failed to position' in e.msg: 226 | return False 227 | else: 228 | raise e 229 | except Exception as e: 230 | raise e 231 | 232 | def wait_gone(self, locate, index=0, max_times=20, delay=0.5, displayed=True): 233 | """ 234 | 等待元素消失 235 | :param locate: 元素定位 236 | :param max_times: 最大循环次数,默认为20次 237 | :param index: 序号,第一个为0,第二个为1,以此类推,默认为0 238 | :param delay: 延时,默认为0.5秒 239 | :param displayed: 是否必须等待到元素可见才算找到元素,默认为是 240 | :return: 241 | """ 242 | for i in range(max_times): 243 | try: 244 | self.find_element(locate, index, 1, delay, displayed) 245 | if i < max_times-1: 246 | self.sleep(delay) 247 | else: 248 | return False 249 | except WebDriverException as e: 250 | if 'Failed to position' in e.msg: 251 | logger.info("The element {} has been gone.".format(locate)) 252 | return True 253 | else: 254 | raise e 255 | except Exception as e: 256 | raise e 257 | 258 | 259 | def exists(self, locate, index=0, displayed=True): 260 | """ 261 | 检查当前时刻元素是否存在 262 | :param locate: 263 | :param index: 序号,第一个为0,第二个为1,以此类推,默认为0 264 | :param displayed: 是否必须等待到元素可见才算找到元素,默认为是 265 | :return: 266 | """ 267 | try: 268 | self.find_element(locate, index, max_times=1, displayed=displayed) 269 | return True 270 | except: 271 | return False 272 | 273 | def send_keys(self, locate, text, index=0, max_times=20, delay=0.5, displayed=True): 274 | """ 275 | 输入文本,用于需要定位页面上有多个属性相同的元素中的某一个元素或对应所有元素的情况 276 | :param locate: 元素定位 277 | :param text: 需要输入的文本内容 278 | :param index: 序号,第一个为0,第二个为1,以此类推,默认为0 279 | :param max_times: 最大循环次数,默认为20次 280 | :param delay: 延时,默认为0.5秒 281 | :param displayed: 是否必须等待到元素可见才算找到元素,默认为True 282 | :return: 283 | """ 284 | elements = self.find_element(locate, index, max_times, delay, displayed) 285 | if index != None: 286 | try: 287 | elements.clear() 288 | elements.send_keys(text) 289 | logger.info("SendKeys --> {}[{}] success".format(locate, index)) 290 | except WebDriverException as e: 291 | logger.error("SendKeys --> {}[{}] failure\nFailed to send_keys to element with {}" .format(locate, index, e)) 292 | raise WebDriverException(self.daf.get_screenshot(self.driver)) 293 | else: 294 | for i, element in enumerate(elements): 295 | try: 296 | element.clear() 297 | element.send_keys(text) 298 | logger.info("SendKeys --> {}[{}] success".format(locate, i)) 299 | except WebDriverException as e: 300 | logger.error("SendKeys --> {}[{}] failure\nFailed to send_keys to element with {}" .format(locate, i, e)) 301 | raise WebDriverException(self.daf.get_screenshot(self.driver)) 302 | 303 | def clear(self, locate, index=0, max_times=20, delay=0.5, displayed=True): 304 | """ 305 | 清除文本框,用于需要定位页面上有多个属性相同的元素中的某一个元素或对应所有元素的情况 306 | :param locate: 元素定位 307 | :param index: 序号,第一个为0,第二个为1,以此类推,默认为0 308 | :param max_times: 最大循环次数,默认为20次 309 | :param delay: 延时,默认为0.5秒 310 | :param displayed: 是否必须等待到元素可见才算找到元素,默认为True 311 | :return: 312 | """ 313 | elements = self.find_element(locate, index, max_times, delay, displayed) 314 | if index != None: 315 | try: 316 | elements.clear() 317 | logger.info("Clear --> {}[{}] success".format(locate, index)) 318 | except WebDriverException as e: 319 | logger.error("Clear --> {}[{}] failure\nFailed to clear to element with {}" .format(locate, index, e)) 320 | raise WebDriverException(self.daf.get_screenshot(self.driver)) 321 | else: 322 | for i, element in enumerate(elements): 323 | try: 324 | element.clear() 325 | logger.info("Clear --> {}[{}] success".format(locate, i)) 326 | except WebDriverException as e: 327 | logger.error("Clear --> {}[{}] failure\nFailed to clear to element with {}" .format(locate, i, e)) 328 | raise WebDriverException(self.daf.get_screenshot(self.driver)) 329 | 330 | def click(self, locate, index=0, max_times=20, delay=0.5, displayed=True): 331 | """ 332 | 点击元素,用于需要定位页面上有多个属性相同的元素中的某一个元素或对应所有元素的情况 333 | :param locate: 元素定位 334 | :param index: 序号,第一个为0,第二个为1,以此类推,默认为0 335 | :param max_times: 最大循环次数,默认为20次 336 | :param delay: 延时,默认为0.5秒 337 | :param displayed: 是否必须等待到元素可见才算找到元素,默认为True 338 | :return: 339 | """ 340 | elements = self.find_element(locate, index, max_times, delay, displayed) 341 | if index != None: 342 | try: 343 | elements.click() 344 | logger.info("Click --> {}[{}] success".format(locate, index)) 345 | except ElementNotInteractableException: 346 | self.execute_script("arguments[0].click;", elements) 347 | logger.info("Click --> {}[{}] success".format(locate, index)) 348 | except WebDriverException as e: 349 | logger.error("Click --> {}[{}] failure\nFailed to click the element with {}".format(locate, index, e)) 350 | raise WebDriverException(self.daf.get_screenshot(self.driver)) 351 | else: 352 | for i, element in enumerate(elements): 353 | try: 354 | element.click() 355 | logger.info("Click --> {}[{}] success".format(locate, i)) 356 | except WebDriverException as e: 357 | logger.error("Click --> {}[{}] failure\nFailed to click the element with {}".format(locate, i, e)) 358 | raise WebDriverException(self.daf.get_screenshot(self.driver)) 359 | 360 | def wait_text(self, locate, text, index=0, max_times=20, delay=0.5, displayed=True): 361 | """ 362 | 等待元素的文本内容变成希望出现的文本内容,用于需要定位页面上有多个属性相同的元素中的某一个元素或对应所有元素的情况 363 | :param locate: 元素定位 364 | :param text: 希望出现的文本内容,在本方法中该参数可以是字符串或列表,为字符串时表示等待目标元素列表的文本内容等于text,为列表时表示等待目标元素与text的文本内容一一对应且相等 365 | :param index: 序号,第一个为0,第二个为1,以此类推,默认为None,为默认值时表示等待所有目标元素的文本内容变成希望出现的文本内容 366 | :param max_times: 最大循环次数,默认为20次 367 | :param delay: 延时,默认为0.5秒 368 | :param displayed: 是否必须等待到元素可见才算找到元素,默认为True 369 | :return: 370 | """ 371 | for i in range(max_times): 372 | num = 0 373 | if index != None: 374 | elements_text = self.find_element(locate, index, max_times, delay, displayed).text 375 | else: 376 | elements_text = [element.text for element in self.find_element(locate, index, max_times, delay, displayed)] 377 | logger.info('The text of the target element is "{}",the actual text is "{}"...\nStart matching>>>>>>'.format(text,elements_text)) 378 | if type(text) == list: 379 | if type(elements_text) == list and len(text) != len(elements_text): 380 | logger.error("The length of the entered text list({}) is inconsistent with the actual list({}).".format(len(text), len(elements_text))) 381 | raise Exception("The length of the entered text list({}) is inconsistent with the actual list({}).".format(len(text), len(elements_text))) 382 | elif type(elements_text) == str and len(text) != 1: 383 | logger.error("The length of the entered text list({}) is inconsistent with the actual string(1).".format(len(text))) 384 | raise Exception("The length of the entered text list({}) is inconsistent with the actual string(1).".format(len(text))) 385 | if type(elements_text) == list: 386 | for j,element_text in enumerate(elements_text): 387 | if element_text == text[j]: 388 | num += 1 389 | elif element_text != text[j] and i < max_times-1: 390 | self.sleep(delay) 391 | break 392 | else: 393 | logger.error('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 394 | raise WebDriverException('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 395 | if num == len(elements_text): 396 | logger.info('Matching success!') 397 | break 398 | elif type(elements_text) == str: 399 | if elements_text == text[0]: 400 | logger.info('Matching success!') 401 | break 402 | elif i < max_times-1: 403 | self.sleep(delay) 404 | else: 405 | logger.error('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 406 | raise WebDriverException('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 407 | elif type(text) == str: 408 | if type(elements_text) == list: 409 | for j, element_text in enumerate(elements_text): 410 | if element_text == text: 411 | num += 1 412 | elif element_text != text and i < max_times - 1: 413 | self.sleep(delay) 414 | break 415 | else: 416 | logger.error('Matching Failed!\nTarget : {} \nActual : {}'.format(text, element_text)) 417 | raise WebDriverException('Matching Failed!\nTarget : {} \nActual : {}'.format(text, element_text)) 418 | if num == len(elements_text): 419 | logger.info('Matching success!') 420 | break 421 | elif type(elements_text) == str: 422 | if elements_text == text: 423 | logger.info('Matching success!') 424 | break 425 | elif i < max_times-1: 426 | self.sleep(delay) 427 | else: 428 | logger.error('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 429 | raise WebDriverException('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 430 | else: 431 | logger.warn('Entered the wrong value of text, please check the value of it !\ntext type:{}\ntext value:{}'.format(type(text),text)) 432 | raise Exception('Entered the wrong value of text, please check the value of it !\ntext type:{}\ntext value:{}'.format(type(text),text)) 433 | 434 | def wait_text_gone(self, locate, text, index=0, max_times=20, delay=0.5, displayed=True): 435 | """ 436 | 等待元素的文本内容变更 437 | :param locate: 元素定位 438 | :param text: 希望消失的文本内容,在本方法中该参数可以是字符串或列表,为字符串时表示等待目标元素的文本内容不包含text,为列表时表示等待目标元素与text的文本内容一一对应且不相等 439 | :param index: 序号,第一个为0,第二个为1,以此类推,默认为None,为默认值时表示等待所有目标元素的文本内容变更 440 | :param max_times: 最大循环次数,默认为20次 441 | :param delay: 延时,默认为0.5秒 442 | :param displayed: 是否必须等待到元素可见才算找到元素,默认为True 443 | :return: 444 | """ 445 | for i in range(max_times): 446 | num = 0 447 | try: 448 | if index != None: 449 | elements_text = self.find_element(locate, index, max_times, delay, displayed).text 450 | else: 451 | elements_text = [element.text for element in self.find_element(locate, index, max_times, delay, displayed)] 452 | except WebDriverException as e: 453 | if i < max_times-1: 454 | continue 455 | else: 456 | raise e 457 | logger.info('The text of the target element is "{}",the actual text is "{}"...\nStart matching>>>>>>'.format(text,elements_text)) 458 | if type(text) == list: 459 | if type(elements_text) == list and len(text) != len(elements_text): 460 | logger.info('Matching success!') 461 | break 462 | if type(elements_text) == str and len(text) != 1: 463 | logger.error("The length of the entered text list({}) is inconsistent with the actual string(1).".format(len(text))) 464 | raise Exception("The length of the entered text list({}) is inconsistent with the actual string(1).".format(len(text))) 465 | if type(elements_text) == list: 466 | for j,element_text in enumerate(elements_text): 467 | if element_text != text[j]: 468 | num += 1 469 | elif element_text == text[j] and i < max_times-1: 470 | self.sleep(delay) 471 | break 472 | else: 473 | logger.error('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 474 | raise WebDriverException('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 475 | if num == len(elements_text): 476 | logger.info('Matching success!') 477 | break 478 | elif type(elements_text) == str: 479 | if elements_text != text[0]: 480 | logger.info('Matching success!') 481 | break 482 | elif i < max_times-1: 483 | self.sleep(delay) 484 | else: 485 | logger.error('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 486 | raise WebDriverException('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 487 | elif type(text) == str: 488 | if type(elements_text) == list: 489 | for j, element_text in enumerate(elements_text): 490 | if element_text != text: 491 | num += 1 492 | elif element_text == text and i < max_times - 1: 493 | self.sleep(delay) 494 | break 495 | else: 496 | logger.error('Matching Failed!\nTarget : {} \nActual : {}'.format(text, element_text)) 497 | raise WebDriverException('Matching Failed!\nTarget : {} \nActual : {}'.format(text, element_text)) 498 | if num == len(elements_text): 499 | logger.info('Matching success!') 500 | break 501 | elif type(elements_text) == str: 502 | if elements_text != text: 503 | logger.info('Matching success!') 504 | break 505 | elif i < max_times-1: 506 | self.sleep(delay) 507 | else: 508 | logger.error('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 509 | raise WebDriverException('Matching Failed!\nTarget : {} \nActual : {}'.format(text, elements_text)) 510 | else: 511 | logger.warn('Entered the wrong value of text, please check the value of it !\ntext type:{}\ntext value:{}'.format(type(text),text)) 512 | raise Exception('Entered the wrong value of text, please check the value of it !\ntext type:{}\ntext value:{}'.format(type(text),text)) 513 | 514 | if __name__ == '__main__': 515 | if 1 and None or 1 and None: 516 | print(1) -------------------------------------------------------------------------------- /framework/HTMLTestReportCN.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | A TestRunner for use with the Python unit testing framework. It 4 | generates a HTML report to show the result at a glance. 5 | 6 | The simplest way to use this is to invoke its main method. E.g. 7 | 8 | import unittest 9 | import HTMLTestRunner 10 | 11 | ... define your tests ... 12 | 13 | if __name__ == '__main__': 14 | HTMLTestRunner.main() 15 | 16 | 17 | For more customization options, instantiates a HTMLTestRunner object. 18 | HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g. 19 | 20 | # output to a file 21 | fp = file('my_report.html', 'wb') 22 | runner = HTMLTestRunner.HTMLTestRunner( 23 | stream=fp, 24 | title='My unit test', 25 | description='This demonstrates the report output by HTMLTestRunner.' 26 | ) 27 | 28 | # Use an external stylesheet. 29 | # See the Template_mixin class for more customizable options 30 | runner.STYLESHEET_TMPL = '' 31 | 32 | # run the test 33 | runner.run(my_test_suite) 34 | 35 | 36 | ------------------------------------------------------------------------ 37 | Copyright (c) 2004-2007, Wai Yip Tung 38 | All rights reserved. 39 | 40 | Redistribution and use in source and binary forms, with or without 41 | modification, are permitted provided that the following conditions are 42 | met: 43 | 44 | * Redistributions of source code must retain the above copyright notice, 45 | this list of conditions and the following disclaimer. 46 | * Redistributions in binary form must reproduce the above copyright 47 | notice, this list of conditions and the following disclaimer in the 48 | documentation and/or other materials provided with the distribution. 49 | * Neither the name Wai Yip Tung nor the names of its contributors may be 50 | used to endorse or promote products derived from this software without 51 | specific prior written permission. 52 | 53 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 54 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 55 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 56 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 57 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 58 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 59 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 60 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 61 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 62 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 63 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 64 | """ 65 | 66 | # URL: http://tungwaiyip.info/software/HTMLTestRunner.html 67 | # URL: https://github.com/Gelomen/HTMLTestReportCN-ScreenShot 68 | 69 | __author__ = "Wai Yip Tung, Findyou, boafantasy, Gelomen" 70 | __version__ = "1.2.0" 71 | 72 | 73 | """ 74 | Change History 75 | Version 1.2.0 -- Gelomen 76 | * 优化用例说明显示 77 | * 错误和失败报告里可以放入多张截图 78 | 79 | Version 1.1.0 -- Gelomen 80 | * 优化报告截图写入方式 81 | 82 | Version 1.0.2 -- Gelomen 83 | * 新增测试结果统计饼图 84 | * 优化筛选时只显示预览 85 | 86 | Version 1.0.1 -- Gelomen 87 | * 修复报告存入文件夹的bug 88 | * 优化报告的命名方式 89 | 90 | Version 1.0.0 -- Gelomen 91 | * 修改测试报告文件夹路径的获取方式 92 | * 修改截图获取文件夹路径的获取方式 93 | 94 | Version 0.9.9 -- Gelomen 95 | * 优化报告文件夹命名 96 | * 优化截图存放的目录 97 | * 增加图片阴影边框以突出图片 98 | * 优化 失败用例合集 和 错误用例合集 显示的颜色 99 | 100 | Version 0.9.8 -- Gelomen 101 | * 优化回到顶部按钮的显示方式 102 | 103 | Version 0.9.7 -- Gelomen 104 | * 优化截图显示,滚动页面会固定居中 105 | 106 | Version 0.9.6 -- Gelomen 107 | * 新增打开图片的特效,可以直接在当前页面看截图 108 | 109 | Version 0.9.5 -- Gelomen 110 | * heading新增 失败 和 错误 测试用例合集 111 | 112 | Version 0.9.4 -- Gelomen 113 | * 修复失败和错误用例里对应按钮的颜色 114 | 115 | Version 0.9.3 -- Gelomen 116 | * 修复点击失败或错误按钮后,浏览器版本和截图的列不会隐藏的bug 117 | 118 | Version 0.9.2 -- Gelomen 119 | * 美化 浏览器版本 和 截图 的显示 120 | 121 | Version 0.9.1 -- Gelomen 122 | * 使用UI自动化测试时,增加 错误、失败 详细信息的 浏览器类型和版本 123 | 124 | Version 0.9.0 -- Gelomen 125 | * 可通过 `need_screenshot=1` 作为开关,将报告开启截图功能 126 | * 增加 失败 和 错误 详细信息的 截图链接 127 | 128 | Version 0.8.4 -- Gelomen 129 | * 删除 失败模块 的显示 130 | 131 | Version 0.8.3 -- Gelomen 132 | * 修复 测试结果 的筛选 133 | * 优化 失败、错误 小图标的颜色 134 | * 增加表格 最后一列 的显示,以美化表格 135 | 136 | Version 0.8.2.1 -Findyou 137 | * 改为支持python3 138 | 139 | Version 0.8.2.1 -Findyou 140 | * 支持中文,汉化 141 | * 调整样式,美化(需要连入网络,使用的百度的Bootstrap.js) 142 | * 增加 通过分类显示、测试人员、通过率的展示 143 | * 优化“详细”与“收起”状态的变换 144 | * 增加返回顶部的锚点 145 | 146 | Version 0.8.2 147 | * Show output inline instead of popup window (Viorel Lupu). 148 | 149 | Version in 0.8.1 150 | * Validated XHTML (Wolfgang Borgert). 151 | * Added description of test classes and test cases. 152 | 153 | Version in 0.8.0 154 | * Define Template_mixin class for customization. 155 | * Workaround a IE 6 bug that it does not treat 293 | 294 | 295 | 296 | %(stylesheet)s 297 | 298 | 299 | 565 | %(heading)s 566 | %(report)s 567 | %(ending)s 568 | 569 | 570 | 571 | """ 572 | # variables: (title, generator, stylesheet, heading, report, ending) 573 | 574 | # ------------------------------------------------------------------------ 575 | # Stylesheet 576 | # 577 | # alternatively use a for external style sheet, e.g. 578 | # 579 | 580 | STYLESHEET_TMPL = """ 581 | 671 | """ 672 | 673 | # ------------------------------------------------------------------------ 674 | # Heading 675 | # 676 | 677 | # 添加显示截图 和 饼状图 的div -- Gelomen 678 | HEADING_TMPL = """
679 |
680 |
681 |

%(title)s

682 | %(parameters)s 683 |

%(description)s

684 |
685 |
686 |
687 | 688 | """ # variables: (title, parameters, description) 689 | 690 | HEADING_ATTRIBUTE_TMPL = """

%(name)s : %(value)s

691 | """ # variables: (name, value) 692 | 693 | # ------------------------------------------------------------------------ 694 | # Report 695 | # 696 | # 汉化,加美化效果 --Findyou 697 | REPORT_TMPL = """ 698 |
699 |

700 | 概要{ %(passrate)s } 701 | 通过{ %(Pass)s } 702 | 失败{ %(fail)s } 703 | 错误{ %(error)s } 704 | 所有{ %(count)s } 705 |

706 |
707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | %(test_list)s 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 |
用例集/测试用例说明总计通过失败错误耗时详细
总计%(count)s%(Pass)s%(fail)s%(error)s%(time_usage)s通过率:%(passrate)s
739 | """ # variables: (test_list, count, Pass, fail, error ,passrate) 740 | 741 | REPORT_CLASS_TMPL = r""" 742 | 743 | %(name)s 744 | %(doc)s 745 | %(count)s 746 | %(Pass)s 747 | %(fail)s 748 | %(error)s 749 | %(time_usage)s 750 | 详细 751 | 752 | """ # variables: (style, desc, count, Pass, fail, error, cid) 753 | 754 | # 失败 的样式,去掉原来JS效果,美化展示效果 -Findyou / 美化类名上下居中,有截图列 -- Gelomen 755 | REPORT_TEST_WITH_OUTPUT_TMPL_1 = r""" 756 | 757 |
%(name)s
758 | %(doc)s 759 | 760 | 763 | 764 | 765 | 766 |
767 |
 768 |     %(script)s
 769 |     
770 |
771 | 772 |
浏览器版本:
%(browser)s

截图:%(screenshot)s
773 | 774 | """ # variables: (tid, Class, style, desc, status) 775 | 776 | # 失败 的样式,去掉原来JS效果,美化展示效果 -Findyou / 美化类名上下居中,无截图列 -- Gelomen 777 | REPORT_TEST_WITH_OUTPUT_TMPL_0 = r""" 778 | 779 |
%(name)s
780 | %(doc)s 781 | 782 | 785 | 786 | 787 | 788 |
789 |
 790 |         %(script)s
 791 |         
792 |
793 | 794 | 795 | 796 | """ # variables: (tid, Class, style, desc, status) 797 | 798 | # 通过 的样式,加标签效果 -Findyou / 美化类名上下居中 -- Gelomen 799 | REPORT_TEST_NO_OUTPUT_TMPL = r""" 800 | 801 |
%(name)s
802 | %(doc)s 803 | %(status)s 804 | 805 | 806 | """ # variables: (tid, Class, style, desc, status) 807 | 808 | REPORT_TEST_OUTPUT_TMPL = r""" 809 | %(id)s: %(output)s 810 | """ # variables: (id, output) 811 | 812 | # ------------------------------------------------------------------------ 813 | # ENDING 814 | # 815 | # 增加返回顶部按钮 --Findyou 816 | ENDING_TMPL = """
 
817 | 820 | """ 821 | 822 | # -------------------- The end of the Template class ------------------- 823 | 824 | 825 | TestResult = unittest.TestResult 826 | 827 | 828 | class _TestResult(TestResult): 829 | # note: _TestResult is a pure representation of results. 830 | # It lacks the output and reporting ability compares to unittest._TextTestResult. 831 | 832 | def __init__(self, verbosity=1): 833 | TestResult.__init__(self) 834 | self.stdout0 = None 835 | self.stderr0 = None 836 | self.success_count = 0 837 | self.failure_count = 0 838 | self.error_count = 0 839 | self.verbosity = verbosity 840 | 841 | # result is a list of result in 4 tuple 842 | # ( 843 | # result code (0: success; 1: fail; 2: error), 844 | # TestCase object, 845 | # Test output (byte string), 846 | # stack trace, 847 | # ) 848 | self.result = [] 849 | # 增加一个测试通过率 --Findyou 850 | self.passrate = float(0) 851 | 852 | # 增加失败用例合集 853 | self.failCase = "" 854 | # 增加错误用例合集 855 | self.errorCase = "" 856 | 857 | def startTest(self, test): 858 | stream = sys.stderr 859 | # stdout_content = " Testing: " + str(test) 860 | # stream.write(stdout_content) 861 | # stream.flush() 862 | # stream.write("\n") 863 | TestResult.startTest(self, test) 864 | # just one buffer for both stdout and stderr 865 | self.outputBuffer = io.StringIO() 866 | stdout_redirector.fp = self.outputBuffer 867 | stderr_redirector.fp = self.outputBuffer 868 | self.stdout0 = sys.stdout 869 | self.stderr0 = sys.stderr 870 | sys.stdout = stdout_redirector 871 | sys.stderr = stderr_redirector 872 | self.test_start_time = round(time.time(), 2) 873 | 874 | def complete_output(self): 875 | """ 876 | Disconnect output redirection and return buffer. 877 | Safe to call multiple times. 878 | """ 879 | self.test_end_time = round(time.time(), 2) 880 | if self.stdout0: 881 | sys.stdout = self.stdout0 882 | sys.stderr = self.stderr0 883 | self.stdout0 = None 884 | self.stderr0 = None 885 | return self.outputBuffer.getvalue() 886 | 887 | def stopTest(self, test): 888 | # Usually one of addSuccess, addError or addFailure would have been called. 889 | # But there are some path in unittest that would bypass this. 890 | # We must disconnect stdout in stopTest(), which is guaranteed to be called. 891 | self.complete_output() 892 | 893 | def addSuccess(self, test): 894 | self.success_count += 1 895 | TestResult.addSuccess(self, test) 896 | output = self.complete_output() 897 | use_time = round(self.test_end_time - self.test_start_time, 2) 898 | self.result.append((0, test, output, '', use_time)) 899 | if self.verbosity > 1: 900 | sys.stderr.write(' S ') 901 | sys.stderr.write(str(test)) 902 | sys.stderr.write('\n') 903 | else: 904 | sys.stderr.write(' S ') 905 | sys.stderr.write('\n') 906 | 907 | def addError(self, test, err): 908 | self.error_count += 1 909 | TestResult.addError(self, test, err) 910 | _, _exc_str = self.errors[-1] 911 | output = self.complete_output() 912 | use_time = round(self.test_end_time - self.test_start_time, 2) 913 | self.result.append((2, test, output, _exc_str, use_time)) 914 | if self.verbosity > 1: 915 | sys.stderr.write(' E ') 916 | sys.stderr.write(str(test)) 917 | sys.stderr.write('\n') 918 | else: 919 | sys.stderr.write(' E ') 920 | sys.stderr.write('\n') 921 | 922 | # 添加收集错误用例名字 -- Gelomen 923 | self.errorCase += "
  • " + str(test) + "
  • " 924 | 925 | def addFailure(self, test, err): 926 | self.failure_count += 1 927 | TestResult.addFailure(self, test, err) 928 | _, _exc_str = self.failures[-1] 929 | output = self.complete_output() 930 | use_time = round(self.test_end_time - self.test_start_time, 2) 931 | self.result.append((1, test, output, _exc_str, use_time)) 932 | if self.verbosity > 1: 933 | sys.stderr.write(' F ') 934 | sys.stderr.write(str(test)) 935 | sys.stderr.write('\n') 936 | else: 937 | sys.stderr.write(' F ') 938 | sys.stderr.write('\n') 939 | 940 | # 添加收集失败用例名字 -- Gelomen 941 | self.failCase += "
  • " + str(test) + "
  • " 942 | 943 | 944 | # 新增 need_screenshot 参数,-1为无需截图,否则需要截图 -- Gelomen 945 | class HTMLTestRunner(Template_mixin): 946 | """ 947 | """ 948 | 949 | def __init__(self, stream=sys.stdout, verbosity=2, title=None, description=None, tester=None): 950 | self.need_screenshot = 0 951 | self.stream = stream 952 | self.verbosity = verbosity 953 | if title is None: 954 | self.title = self.DEFAULT_TITLE 955 | else: 956 | self.title = title 957 | if description is None: 958 | self.description = self.DEFAULT_DESCRIPTION 959 | else: 960 | self.description = description 961 | if tester is None: 962 | self.tester = self.DEFAULT_TESTER 963 | else: 964 | self.tester = tester 965 | 966 | self.startTime = datetime.datetime.now() 967 | 968 | def run(self, test): 969 | "Run the given test case or test suite." 970 | result = _TestResult(self.verbosity) # verbosity为1,只输出成功与否,为2会输出用例名称 971 | test(result) 972 | self.stopTime = datetime.datetime.now() 973 | self.generateReport(test, result) 974 | # 优化测试结束后打印蓝色提示文字 -- Gelomen 975 | print("\n\033[36;0m--------------------- 测试结束 ---------------------\n" 976 | "------------- 合计耗时: %s -------------\033[0m" % (self.stopTime - self.startTime), file=sys.stderr) 977 | return result 978 | 979 | def sortResult(self, result_list): 980 | # unittest does not seems to run in any particular order. 981 | # Here at least we want to group them together by class. 982 | rmap = {} 983 | classes = [] 984 | for n, t, o, e, s in result_list: 985 | cls = t.__class__ 986 | if cls not in rmap: 987 | rmap[cls] = [] 988 | classes.append(cls) 989 | rmap[cls].append((n, t, o, e, s)) 990 | r = [(cls, rmap[cls]) for cls in classes] 991 | return r 992 | 993 | # 替换测试结果status为通过率 --Findyou 994 | def getReportAttributes(self, result): 995 | """ 996 | Return report attributes as a list of (name, value). 997 | Override this to add custom attributes. 998 | """ 999 | startTime = str(self.startTime)[:19] 1000 | duration = str(self.stopTime - self.startTime) 1001 | status = [] 1002 | status.append('共 %s' % (result.success_count + result.failure_count + result.error_count)) 1003 | if result.success_count: 1004 | status.append('通过 %s' % result.success_count) 1005 | if result.failure_count: 1006 | status.append('失败 %s' % result.failure_count) 1007 | if result.error_count: 1008 | status.append('错误 %s' % result.error_count) 1009 | if status: 1010 | status = ','.join(status) 1011 | if (result.success_count + result.failure_count + result.error_count) > 0: 1012 | self.passrate = str("%.2f%%" % (float(result.success_count) / float(result.success_count + result.failure_count + result.error_count) * 100)) 1013 | else: 1014 | self.passrate = "0.00 %" 1015 | else: 1016 | status = 'none' 1017 | 1018 | if len(result.failCase) > 0: 1019 | failCase = result.failCase 1020 | else: 1021 | failCase = "无" 1022 | 1023 | if len(result.errorCase) > 0: 1024 | errorCase = result.errorCase 1025 | else: 1026 | errorCase = "无" 1027 | 1028 | return [ 1029 | ('测试人员', self.tester), 1030 | ('开始时间', startTime), 1031 | ('合计耗时', duration), 1032 | ('测试结果', status + ",通过率 = " + self.passrate), 1033 | ('失败用例合集', failCase), 1034 | ('错误用例合集', errorCase), 1035 | ] 1036 | 1037 | def generateReport(self, test, result): 1038 | report_attrs = self.getReportAttributes(result) 1039 | generator = 'HTMLTestRunner %s' % __version__ 1040 | stylesheet = self._generate_stylesheet() 1041 | # 添加 通过、失败 和 错误 的统计,以用于饼图 -- Gelomen 1042 | Pass = self._generate_report(result)["Pass"] 1043 | fail = self._generate_report(result)["fail"] 1044 | error = self._generate_report(result)["error"] 1045 | 1046 | heading = self._generate_heading(report_attrs) 1047 | report = self._generate_report(result)["report"] 1048 | ending = self._generate_ending() 1049 | output = self.HTML_TMPL % dict( 1050 | title=saxutils.escape(self.title), 1051 | generator=generator, 1052 | stylesheet=stylesheet, 1053 | Pass=Pass, 1054 | fail=fail, 1055 | error=error, 1056 | heading=heading, 1057 | report=report, 1058 | ending=ending, 1059 | ) 1060 | self.stream.write(output.encode('utf8')) 1061 | 1062 | def _generate_stylesheet(self): 1063 | return self.STYLESHEET_TMPL 1064 | 1065 | # 增加Tester显示 -Findyou 1066 | # 增加 失败用例合集 和 错误用例合集 的显示 -- Gelomen 1067 | def _generate_heading(self, report_attrs): 1068 | a_lines = [] 1069 | for name, value in report_attrs: 1070 | # 如果是 失败用例 或 错误用例合集,则不进行转义 -- Gelomen 1071 | if name == "失败用例合集": 1072 | if value == "无": 1073 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1074 | name=name, 1075 | value="
      " + value + "
    ", 1076 | ) 1077 | else: 1078 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1079 | name=name, 1080 | value="
    点击查看
    " 1081 | "
      " + value + "
    ", 1082 | ) 1083 | elif name == "错误用例合集": 1084 | if value == "无": 1085 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1086 | name=name, 1087 | value="
      " + value + "
    ", 1088 | ) 1089 | else: 1090 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1091 | name=name, 1092 | value="
    点击查看
    " 1093 | "
      " + value + "
    ", 1094 | ) 1095 | else: 1096 | line = self.HEADING_ATTRIBUTE_TMPL % dict( 1097 | name=saxutils.escape(name), 1098 | value=saxutils.escape(value), 1099 | ) 1100 | a_lines.append(line) 1101 | heading = self.HEADING_TMPL % dict( 1102 | title=saxutils.escape(self.title), 1103 | parameters=''.join(a_lines), 1104 | description=saxutils.escape(self.description), 1105 | tester=saxutils.escape(self.tester), 1106 | ) 1107 | return heading 1108 | 1109 | # 生成报告 --Findyou添加注释 1110 | def _generate_report(self, result): 1111 | rows = [] 1112 | sortedResult = self.sortResult(result.result) 1113 | # 所有用例统计耗时初始化 1114 | sum_ns = 0 1115 | for cid, (cls, cls_results) in enumerate(sortedResult): 1116 | # subtotal for a class 1117 | np = nf = ne = ns = 0 1118 | for n, t, o, e, s in cls_results: 1119 | if n == 0: 1120 | np += 1 1121 | elif n == 1: 1122 | nf += 1 1123 | elif n == 2: 1124 | ne += 1 1125 | ns += s # 把单个class用例文件里面的多个def用例每次的耗时相加 1126 | ns = round(ns, 2) 1127 | sum_ns += ns # 把所有用例的每次耗时相加 1128 | # format class description 1129 | # if cls.__module__ == "__main__": 1130 | # name = cls.__name__ 1131 | # else: 1132 | # name = "%s.%s" % (cls.__module__, cls.__name__) 1133 | name = cls.__name__ 1134 | doc = cls.__doc__ and cls.__doc__.split("\n")[0] or "" 1135 | # desc = doc and '%s - %s' % (name, doc) or name 1136 | 1137 | row = self.REPORT_CLASS_TMPL % dict( 1138 | style=ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass', 1139 | name=name, 1140 | doc=doc, 1141 | count=np + nf + ne, 1142 | Pass=np, 1143 | fail=nf, 1144 | error=ne, 1145 | cid='c%s' % (cid + 1), 1146 | time_usage=str(ns) + "秒" # 单个用例耗时 1147 | ) 1148 | rows.append(row) 1149 | 1150 | for tid, (n, t, o, e, s) in enumerate(cls_results): 1151 | self._generate_report_test(rows, cid, tid, n, t, o, e) 1152 | sum_ns = round(sum_ns, 2) 1153 | report = self.REPORT_TMPL % dict( 1154 | test_list=''.join(rows), 1155 | count=str(result.success_count + result.failure_count + result.error_count), 1156 | Pass=str(result.success_count), 1157 | fail=str(result.failure_count), 1158 | error=str(result.error_count), 1159 | time_usage=str(sum_ns) + "秒", # 所有用例耗时 1160 | passrate=self.passrate, 1161 | ) 1162 | 1163 | # 获取 通过、失败 和 错误 的统计并return,以用于饼图 -- Gelomen 1164 | Pass = str(result.success_count) 1165 | fail = str(result.failure_count) 1166 | error = str(result.error_count) 1167 | return {"report": report, "Pass": Pass, "fail": fail, "error": error} 1168 | 1169 | def _generate_report_test(self, rows, cid, tid, n, t, o, e): 1170 | # e.g. 'pt1_1', 'ft1_1', 'et1_1'etc 1171 | has_output = bool(o or e) 1172 | # ID修改点为下划线,支持Bootstrap折叠展开特效 - Findyou 1173 | if n == 0: 1174 | tid_flag = 'p' 1175 | elif n == 1: 1176 | tid_flag = 'f' 1177 | elif n == 2: 1178 | tid_flag = 'e' 1179 | tid = tid_flag + 't%s_%s' % (cid + 1, tid + 1) 1180 | name = t.id().split('.')[-1] 1181 | doc = t.shortDescription() or "" 1182 | # desc = doc and ('%s - %s' % (name, doc)) or name 1183 | 1184 | # utf-8 支持中文 - Findyou 1185 | # o and e should be byte string because they are collected from stdout and stderr? 1186 | if isinstance(o, str): 1187 | # TODO: some problem with 'string_escape': it escape \n and mess up formating 1188 | # uo = unicode(o.encode('string_escape')) 1189 | # uo = o.decode('latin-1') 1190 | uo = o 1191 | else: 1192 | uo = o 1193 | if isinstance(e, str): 1194 | # TODO: some problem with 'string_escape': it escape \n and mess up formating 1195 | # ue = unicode(e.encode('string_escape')) 1196 | # ue = e.decode('latin-1') 1197 | ue = e 1198 | else: 1199 | ue = e 1200 | 1201 | script = self.REPORT_TEST_OUTPUT_TMPL % dict( 1202 | id=tid, 1203 | output=saxutils.escape(uo + ue), 1204 | ) 1205 | 1206 | # 截图名字通过抛出异常存放在u,通过截取字段获得截图名字 -- Gelomen 1207 | u = uo + ue 1208 | # 先判断是否需要截图 1209 | self.need_screenshot = u.find("errorImg[") 1210 | 1211 | if self.need_screenshot == -1: 1212 | tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL_0 or self.REPORT_TEST_NO_OUTPUT_TMPL 1213 | 1214 | row = tmpl % dict( 1215 | tid=tid, 1216 | Class=(n == 0 and 'hiddenRow' or 'none'), 1217 | style=n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'), 1218 | name=name, 1219 | doc=doc, 1220 | script=script, 1221 | status=self.STATUS[n], 1222 | ) 1223 | else: 1224 | tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL_1 or self.REPORT_TEST_NO_OUTPUT_TMPL 1225 | 1226 | screenshot_list = re.findall("errorImg\[(.*?)\]errorImg", u) 1227 | screenshot = "" 1228 | for i in screenshot_list: 1229 | screenshot += "
    img_" + i + "" 1230 | 1231 | # screenshot = u[u.find('errorImg[') + 9:u.find(']errorImg')] 1232 | browser = u[u.find('browser[') + 8:u.find(']browser')] 1233 | 1234 | row = tmpl % dict( 1235 | tid=tid, 1236 | Class=(n == 0 and 'hiddenRow' or 'none'), 1237 | style=n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'passCase'), 1238 | name=name, 1239 | doc=doc, 1240 | script=script, 1241 | status=self.STATUS[n], 1242 | # 添加截图字段 1243 | screenshot=screenshot, 1244 | # 添加浏览器版本字段 1245 | browser=browser 1246 | ) 1247 | rows.append(row) 1248 | 1249 | if not has_output: 1250 | return 1251 | 1252 | def _generate_ending(self): 1253 | return self.ENDING_TMPL 1254 | 1255 | 1256 | # 集成创建文件夹、保存截图、获得截图名字等方法,与HTMLTestReportCN交互从而实现嵌入截图 -- Gelomen 1257 | class DirAndFiles(object): 1258 | 1259 | def __init__(self): 1260 | self.path = os.path.join(os.path.dirname(os.path.dirname(__file__)),"test_report/") 1261 | self.title = "Test Report" 1262 | 1263 | def create_dir(self, title=None): 1264 | 1265 | now_2d = time.strftime("%Y-%m-%d", time.localtime(time.time())) 1266 | now_2s = time.strftime("%Y-%m-%d_%H_%M_%S", time.localtime(time.time())) 1267 | 1268 | if title is not None: 1269 | self.title = title 1270 | 1271 | dir_path = self.path + self.title + now_2d + "/" + self.title + now_2s 1272 | # 判断文件夹是否存在,不存在则创建 1273 | if os.path.isdir(dir_path): 1274 | pass 1275 | else: 1276 | os.makedirs(dir_path) 1277 | 1278 | # 测试报告路径 1279 | report_path = dir_path + "/" + "自动化测试报告.html" 1280 | 1281 | # 将新建的 文件夹路径 和 报告路径 存入全局变量 1282 | GlobalMsg.set_value("dir_path", dir_path) 1283 | GlobalMsg.set_value("report_path", report_path) 1284 | 1285 | @staticmethod 1286 | def get_screenshot(browser): 1287 | i = 1 1288 | 1289 | # 通过全局变量获取文件夹路径 1290 | new_dir = GlobalMsg.get_value("dir_path") 1291 | 1292 | img_dir = new_dir + "/image" 1293 | # 判断文件夹是否存在,不存在则创建 1294 | is_dir = os.path.isdir(img_dir) 1295 | if not is_dir: 1296 | os.makedirs(img_dir) 1297 | 1298 | img_path = img_dir + "/" + str(i) + ".png" 1299 | 1300 | # 有可能同个测试步骤出错,截图名字一样导致覆盖文件,所以名字存在则增加id 1301 | while True: 1302 | is_file = os.path.isfile(img_path) 1303 | if is_file: 1304 | i += 1 1305 | img_path = img_dir + "/" + str(i) + ".png" 1306 | else: 1307 | break 1308 | 1309 | browser.get_screenshot_as_file(img_path) 1310 | img_name = str(i) + ".png" 1311 | 1312 | browser_type = browser.capabilities["browserName"] 1313 | browser_version = browser.capabilities["version"] 1314 | browser_msg = browser_type + "(" + browser_version + ")" 1315 | 1316 | print("errorImg[" + img_name + "]errorImg, browser[" + browser_msg + "]browser") 1317 | 1318 | 1319 | ############################################################################## 1320 | # Facilities for running tests from the command line 1321 | ############################################################################## 1322 | 1323 | # Note: Reuse unittest.TestProgram to launch test. In the future we may 1324 | # build our own launcher to support more specific command line 1325 | # parameters like test title, CSS, etc. 1326 | class TestProgram(unittest.TestProgram): 1327 | """ 1328 | A variation of the unittest.TestProgram. Please refer to the base 1329 | class for command line parameters. 1330 | """ 1331 | 1332 | def runTests(self): 1333 | # Pick HTMLTestRunner as the default test runner. 1334 | # base class's testRunner parameter is not useful because it means 1335 | # we have to instantiate HTMLTestRunner before we know self.verbosity. 1336 | if self.testRunner is None: 1337 | self.testRunner = HTMLTestRunner(verbosity=self.verbosity) 1338 | unittest.TestProgram.runTests(self) 1339 | 1340 | 1341 | main = TestProgram 1342 | 1343 | ############################################################################## 1344 | # Executing this module from the command line 1345 | ############################################################################## 1346 | 1347 | if __name__ == "__main__": 1348 | main(module=None) 1349 | --------------------------------------------------------------------------------