在第三節基礎上多了下面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)
截圖中提到的工具類和配置代碼如下
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
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
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'])
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())
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
主入口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)