在第三节基础上多了下面5个层级(具体层级可参考下图)
components层: 组件层,放置UI自动化公共组件(比如selenium的操作类)以及页面组件脚本(比如多个页面脚本相同,可以用组件形式存储,避免重复工作)
config层: 配置层,管理系统配置
log层: 日志层,放置UI自动化运行的日志信息
page层: 页面层,放置UI自动化页面操作脚本
screenshots层: 截图层,放置UI自动化运行中捕获的截图
config、log、screenshots不难理解,主要解释下page层和components层怎么来方便我们UI自动化脚本编辑?
1、代码维护层级清晰:
page层: 维护UI脚本页面操作
components层: 维护UI脚本组件
testcases层: 维护测试用例
2、减少重复工作:
page层维护登录、目录层级选择这些公共页面操作,只需要编写一次
components层维护页面操作的公共组件(比如多层目录选择),只需要编写一次
接下来根据上节的test_aaa.py用例文件做下扩展
首先page层,针对 test_aaa.py进行页面操作类封装
aaa.py,具体代码如下
#coding:utf-8 from component.common.webdriver_base import WebDriverBase import time from utils.log_util import LogUtil logger = LogUtil("aaa").get_logger() class TestAaa(WebDriverBase): def login1(self): # 访问百度首页 self.open_url(r"http://www.baidu.com") # self.driver.get(r"http://www.baidu.com") # 百度输入框输入 self.loc_method("kw", "send_keys", method='id', text="懒勺") # self.driver.find_element_by_id("kw").send_keys("懒勺") # 点百度一下 self.loc_method("su", "click", method='id') # self.driver.find_element_by_id("su").click() #等待时间只是为了让你可以看到目前效果,可以省略 time.sleep(2) def login2(self): # 访问qq首页 self.open_url(r"http://www.qq.com") # self.driver.get(r"http://www.qq.com") # 点新闻链接 self.loc_method("//a[text()='新闻']", "click", method='xpath') # self.driver.find_element_by_xpath("//a[text()='新闻']").click() # 等待时间只是为了让你可以看到目前效果,可以省略 time.sleep(3) logger.info("测试login2方法")
test_aaa.py代码变更如下(为什么要把页面操作放到page层?分层方便代码维护,以及2个test类共用了相同的页面操作,可以直接调用,不需要重复维护):
# -*- coding:utf-8 -*- import unittest from page.aaa import TestAaa import time #QingQing类的名字任意命名,但命名()里的unittest.TestCase就是去继承这个类,类的作用就是可以使runner.run识别 class QingQing(unittest.TestCase): #unittest.TestCase类定义的setUpClass和tearDownClass方法前一定要加@classmethod, #setUpClass在这个类里面是第一个执行的方法 #tearDownClass在这个类里面是最后一个执行的方法 #中间的执行顺序是通过字符的大小进行顺序执行,命名必须test_开头 #打开浏览器,获取配置 @classmethod def setUpClass(self): self.aaa = TestAaa() def test_01_search_baidu(self): # 访问百度首页 # 百度输入框输入 # 点百度一下 self.aaa.login1() #执行商品收费功能 def test_02_search_qq_news(self): # 访问qq首页 # 点新闻链接 self.aaa.login2() #退出浏览器 @classmethod def tearDownClass(self): self.aaa.quit_browser() if __name__ == "__main__": unittest.main()
最后components层,对selenium做如下封装(为什么要封装,比如你点击和输入文本操作,一般前提还得考虑元素是否存在才能去点击或输入,这部分重复性工作可以省去)
封装类webdriver_base.py,具体代码如下
# -*- coding:utf-8 -*- from time import sleep import os from selenium.common.exceptions import * from selenium.webdriver import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.support.select import Select from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium import webdriver from utils.datetime_util import DateTimeUtil from utils.log_util import LogUtil from utils.yaml_util import YamlUtil logger = LogUtil('webdriver_base').get_logger() driver = None class WebDriverBase(object): # 页面操作基础类 def __init__(self): global driver # 如果driver不为空,直接使用原来的driver if driver != None: self.driver = driver return # 获取驱动 chromeDriverPath = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'driver', 'chromedriver.exe') option = webdriver.ChromeOptions() option.add_argument("disable-infobars") # 获取配置文件 sysConfig = YamlUtil('sysconfig.yaml').read_yaml() # 找浏览器的名字 browserName = sysConfig['browser']['browserName'] if str(browserName).lower() == 'chrome': # 获取谷歌的驱动 driver = webdriver.Chrome(executable_path=chromeDriverPath, chrome_options=option) self.driver = driver else: logger.error("暂不支持谷歌以外的驱动") raise Exception("暂不支持谷歌以外的驱动") if self.driver == None: logger.error("打开浏览器驱动失败") raise Exception("打开浏览器驱动失败") self.maximize_window() def open_url(self, url): # 访问浏览器地址 self.driver.get(url) def get_driver(self): return self.driver def loc_method(self, eleLoc, action, method='CSS', text=None): """ 通用元素定位方法主入口 :param eleLoc: 定位的元素路径 :param action: 页面动作(输入文本,点击等等) :param method: 定位方式(css, path)提示:id、name、class属性都可以用css定位到,默认为CSS :param text: 如果是需要文本信息输入校验,才需要用到 :return: """ #loc放到selenium的driver.find_element方法就会自动识别元素 if str(method).upper() == 'CSS': loc = (By.CSS_SELECTOR, eleLoc) elif str(method).upper() == 'XPATH': loc = (By.XPATH, eleLoc) elif str(method).upper() == 'ID': loc = (By.ID, eleLoc) elif str(method).upper() == 'NAME': loc = (By.NAME, eleLoc) elif str(method).upper() == 'CLASS': loc = (By.CLASS_NAME, eleLoc) else: loc = None try: if loc != None: if action == 'click': self.click(loc) elif action == 'send_keys': self.send_keys(text, loc) elif action == 'select_by_text': self.select_by_text(text, loc) elif action == 'select_by_index': self.select_by_index(text, loc) elif action == 'select_by_value': self.select_by_value(text, loc) elif action == 'get_element_text': return self.get_element_text(loc) elif action == 'get_element_attribute': return self.get_element_attribute(text, loc) elif action == 'text_in_element': return self.text_in_element(text, loc) elif action == 'value_in_element': return self.value_in_element(text, loc) else: logger.error("action错误:请确认action值:%s" % action) else: logger.error("method错误:请确认method值:%s" % method) except Exception as e: logger.error(e) def send_keys(self, text, loc): # 输入框输入文本信息,先清除文本框内容后输入 self.clear_input_box(loc) try: self.find_element(*loc).send_keys(text) sleep(1) except Exception as e: logger.error(e) self.get_screen_img() raise def clear_input_box(self, loc): # 清除输入框内容 self.find_element(*loc).clear() sleep(1) def click(self, loc): # 点击 try: self.find_element(*loc).click() sleep(2) except Exception as e: logger.error(e) self.get_screen_img() raise def move_to_element(self, *loc): # 鼠标悬停 above = self.find_element(*loc) ActionChains(self.driver).move_to_element(above).perform() def close_single_window(self): # 关闭当前窗口(单个的) self.driver.close() def quit_browser(self): # 退出浏览器,关闭所有窗口 self.driver.quit() def maximize_window(self): # 浏览器窗口最大化 self.driver.maximize_window() def browser_forward(self): # 浏览器前进 self.driver.forward() def browser_back(self): # 浏览器后退 self.driver.back() def browser_refresh(self): # 浏览器刷新 self.driver.refresh() def get_element_text(self, loc): # 获取元素的文本 return self.find_element(*loc).text def get_element_attribute(self, attributeItem, loc): # 获取元素的属性,可以是id,name,type或其他任意属性 return self.find_element(*loc).get_attribute(attributeItem) def implicitly_wait(self, seconds): # 隐式等待时间,最长等待seconds秒,超过抛出超时异常,常用于页面加载等待 self.driver.implicitly_wait(seconds) def select_by_index(self, index, *loc): # 通过index 下标取select ele = self.find_element(*loc) Select(ele).select_by_index(index) sleep(1) def select_by_value(self, value, *loc): # 通过value值取select ele = self.find_element(*loc) Select(ele).select_by_value(value) sleep(1) def select_by_text(self, text, loc): # 通过文本text值取select ele = self.find_element(*loc) Select(ele).select_by_visible_text(text) sleep(1) def text_in_element(self, text, *loc, timeout=10): # 判断某个元素的text是否包含了预期的值 # 没定位到元素返回False,定位到元素返回判断结果布尔值true try: ele = WebDriverWait(self.driver, timeout, 1).until(EC.text_to_be_present_in_element(*loc, text)) except TimeoutException: logger.error("查找超时,%s不在元素的文本里面" % text) return False return ele def value_in_element(self, value, *loc, timeout=10): # 判断某个元素的value是否包含了预期的值 # 没定位到元素返回False,定位到元素返回判断结果布尔值true try: ele = WebDriverWait(self.driver, timeout, 1).until(EC.text_to_be_present_in_element_value(*loc, value)) except TimeoutException: logger.info("查找超时,%s不在元素的value里面" % value) return False return ele def find_element(self, *loc): """ 定位元素 :param loc: 元组 示例:(By.CSS,'id') :return: """ try: WebDriverWait(self.driver, 10).until(lambda driver: driver.find_element(*loc).is_displayed()) return self.driver.find_element(*loc) except NoSuchElementException: logger.error("找不到定位的元素:%s" % loc[1]) raise except TimeoutException: logger.error("元素查找超时:%s" % loc[1]) raise def get_screen_img(self): #截图保存ui运行结果 imgPath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'screenshots') screenName = DateTimeUtil().get_current_time() + '.png' screenFile = os.path.join(imgPath, screenName) try: self.driver.get_screenshot_as_file(screenFile) except Exception as e: logger.error("没有成功截到图,原因是: %s" % e) def switch_to_next_window(self, currentHandle): # 当打开的窗口不是当前窗口,就切换 allHandles = self.driver.window_handles for handle in allHandles: if handle != currentHandle: self.driver.switch_to.window(handle) break def switch_to_next_frame(self, iframe): # 表单切换到iframe,其中iframe是id self.driver.switch_to.frame(iframe) def execute_script(self, js): #执行js命令 self.driver.execute_script(js)
View Code
截图中提到的工具类和配置代码如下
log_util.py
# -*- coding:utf-8 -*- import logging from datetime import datetime import os class LogUtil(): def __init__(self, logname=None): # 日志名称 self.logger = logging.getLogger(logname) # 日志级别 self.logger.setLevel(logging.DEBUG) # 日志输出到控制台 self.console = logging.StreamHandler() self.console.setLevel(logging.DEBUG) # 输出到文件 self.date = datetime.now().strftime("%Y-%m-%d") + '.log' self.filename = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs', self.date) self.file = logging.FileHandler(self.filename, encoding='utf-8') self.file.setLevel(logging.DEBUG) # 日志显示内容 self.formatstr = '%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s %(message)s' self.format = logging.Formatter(self.formatstr) self.console.setFormatter(self.format) self.file.setFormatter(self.format) # 加入到hander self.logger.addHandler(self.console) self.logger.addHandler(self.file) def get_logger(self): return self.logger
View Code
datebase_util.py
# -*- coding:utf-8 -*- from utils.log_util import LogUtil from utils.yaml_util import YamlUtil import pymysql import cx_Oracle logger = LogUtil('database_util').getLogger() class DataBase(object): def __init__(self): pass def queryDataBase(self, querySql): # 获取游标 try: cursor = self.con.cursor() cursor.execute(querySql) return cursor.fetchone()[0] except Exception as e: logger.error(e) finally: self.con.close() def updateData(self, querySql): # 修改数据库数据 try: cursor = self.con.cursor() cursor.execute(querySql) self.con.commit() except Exception as e: self.con.rollback() logger.error(e) finally: self.con.close() class OracleDataBase(DataBase): def __init__(self): sysConfig = YamlUtil('sysconfig.yaml').readYaml() host = sysConfig['oralceConfig']['host'] port = sysConfig['oralceConfig']['port'] user = sysConfig['oralceConfig']['username'] pwd = sysConfig['oralceConfig']['password'] database = sysConfig['oralceConfig']['database'] self.con = cx_Oracle.connect("{}/{}@{}:{}/{}".format(user, pwd, host, port, database).format(), encoding="UTF-8", nencoding="UTF-8") class MysqlDataBase(DataBase): def __init__(self): sysConfig = YamlUtil('sysconfig.yaml').readYaml() host = sysConfig['mysqlConfig']['host'] port = sysConfig['mysqlConfig']['port'] user = sysConfig['mysqlConfig']['username'] pwd = sysConfig['mysqlConfig']['password'] database = sysConfig['mysqlConfig']['database'] self.con = pymysql.Connect( host=host, port=port, user=user, passwd=pwd, db=database, charset='utf8' ) if __name__ == "__main__": pass
View Code
yaml_util.py
# -*- coding:utf-8 -*- import os from ruamel import yaml from utils.log_util import LogUtil logger = LogUtil('yaml_util').get_logger() class YamlUtil(object): def __init__(self, file=None): try: if file != None: self.configPath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config', file) if self.configPath: with open(self.configPath, 'r', encoding='utf-8') as f: self.Yamlobject = yaml.safe_load(f) except Exception as e: logger.error(e) def read_yaml(self): return self.Yamlobject def write_yaml(self, name, value): self.Yamlobject[name] = value with open(self.file, 'w+', encoding='utf-8') as fout: yaml.dump(self.Yamlobject, fout, default_flow_style=False, allow_unicode=True) if __name__ == '__main__': configPath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config', 'sysconfig.yaml') r = YamlUtil(configPath).read_yaml() print(r['browser']['browserName'])
View Code
datetime_util.py
# -*- coding:utf-8 -*- from datetime import datetime class DateTimeUtil(object): def __init__(self): pass def get_current_time(self): return datetime.now().strftime("%Y%m%d%H%M%S") def get_current_date(self): return datetime.now().strftime("%Y-%m-%d") if __name__=="__main__": dateTime = DateTimeUtil() print(dateTime.get_current_time())
View Code
sysconfig.yaml
browser: browserName: chrome login: account: renlk24211 passwd: '12345678' url: https://blade.com.cn mysqlConfig: host: 192.168.160.141 port: 3306 username: root password: 123456 database: auto oracleConfig: host: 192.168.160.141 port: 3306 username: root password: 123456 database: auto db2Config: host: 192.168.160.141 port: 3306 username: root password: 123456 database: auto
View Code
主入口run_all_case.py封装
# -*- coding:utf-8 -*- import unittest import os from utils.HTMLTestRunnerForPy3 import HTMLTestRunner from datetime import datetime class RunAllCase(object): def __init__(self): pass def add_cases(self): # 挑选用例,pattern='test_*.py'表示添加test_开头的py文件 casePath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testcases') discover = unittest.defaultTestLoader.discover( start_dir=casePath, pattern='test_*.py' ) return discover def get_report_file_path(self): # 指定生成报告地址 report_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'reports') report_name = datetime.now().strftime("%Y%m%d%H%M%S") + '.html' report_file = os.path.join(report_path, report_name) return report_file def run_cases(self, report_file, discover, title, description): # 运行用例 runner = HTMLTestRunner( stream=open(report_file, 'wb'), # 生成的html报告标题 title=title, # 1是粗略的报告,2是详细的报告 verbosity=2, # 生成的html描述 description=description ) runner.run(discover) if __name__ == "__main__": r = RunAllCase() discover = r.add_cases() report_file = r.get_report_file_path() title = '银行UI自动化测试报告' description = '银行UI自动化测试报告' r.run_cases(report_file, discover, title, description)
View Code