本工程的 github 地址:https://github.com/juno3550/UIKeywordFramework
1. 關鍵字驅動框架簡介
2. 工程結構說明
3. 工程代碼實現
- action 包
- business_process 包
- util 包
- conf 目錄
- test_data 目錄
- main.py
- test_report 目錄
- exception_pic 目錄
- log 目錄
1. 關鍵字驅動框架簡介
原理及特點
- 關鍵字驅動測試是數據驅動測試的一種改進類型,它也被稱為表格驅動測試或者基於動作字的測試。
- 主要關鍵字包括三類:被操作對象(Item)、操作行為(Operation)和操作值(Value),用面向對象形式可將其表現為 Item.Operation(Value)。
- 將測試邏輯按照這些關鍵字進行分解,形成數據文件。
- 用關鍵字的形式將測試邏輯封裝在數據文件中,測試工具只要能夠解釋這些關鍵字即可對其應用自動化。
優勢
- 執行人員可以不需要太多的技術:一旦框架建立,手工測試人員和非技術人員都可以很容易的編寫自動化測試腳本。
- 簡單易懂:它存在Excel表格中,沒有編碼,測試腳本容易閱讀和理解。關鍵字和操作行為這樣的手工測試用例,使它變得更容易編寫和維護。
- 早期介入:可以在應用未提交測試之前,就可以建立關鍵字驅動測試用例對象庫,從而減少后期工作。使用需求和其它相關文檔進行收集信息,關鍵字數據表可以建立手工測試程序。
- 代碼的重用性:用關鍵字的形式將測試用例及數據進行組裝並解釋執行,提高代碼的可重用性。
2. 工程結構說明
工程結構
整個測試框架分為四層,通過分層的方式,測試代碼更容易理解,維護起來較為方便。
第一層是“測試工具層”:
- util 包:用於實現測試過程中調用的工具類方法,例如讀取配置文件、頁面元素的操作方法、操作 Excel 文件、生成測試報告、發送郵件等。
- conf 包:配置文件及全局變量。
- log 目錄:日志輸出文件。
- exception_pic 目錄:失敗用例的截圖保存目錄。
第二層是“服務層”:相當於對測試對象的一個業務封裝。對於接口測試,是對遠程方法的一個實現;對於 UI 測試,是對頁面元素或操作的一個封裝。
- action 包:封裝具體的頁面動作,如點擊、輸入文本等。
第三層是“測試用例邏輯層”:該層主要是將服務層封裝好的各個業務對象,組織成測試邏輯,進行校驗。
- bussiness_process 包:基於關鍵字的形式,實現單條、多條用例的測試腳本邏輯。
- test_data 目錄:Excel 數據文件,包含用例步驟、被操作對象、操作動作、操作值、測試結果等。
第四層是“測試場景層”:將測試用例組織成測試場景,實現各種級別 cases 的管理,如冒煙,回歸等測試場景。
- main.py:本框架工程的運行主入口。
框架特點
- 基於關鍵字測試框架,即使不懂開發技術的測試人員也可以實施自動化測試,便於在整個測試團隊中推廣和使用自動化測試技術,降低自動化測試實施的技術門檻。
- 使用外部測試數據文件,使用Excel管理測試用例的集合和每個測試用例的所有執行步驟,實現在一個文件中完成測試用例的維護工作。
- 通過定義關鍵字、操作元素的定位方式和定位表達式和操作值,就可以實現每個測試步驟的執行,可以更加靈活地實現自動化測試的需求。
- 基於關鍵字的方式,可以進行任意關鍵字的擴展,以滿足更加復雜的自動化測試需求。
- 實現定位表達式和測試代碼的分離,實現定位表達式直接在數據文件中進行維護。
- 框架提供日志功能,方便調試和監控自動化測試程序的執行。
3. 工程代碼實現
action 包
action 包為框架第二層“服務層”,相當於對測試對象的一個業務封裝。對於接口測試,是對遠程方法的一個實現;對於 UI 測試,是對頁面元素或操作的一個封裝。
page_action.py
該模塊基於關鍵字格式,封裝了頁面操作的常用函數,如打開瀏覽器、點擊、輸入文本等。
1 from selenium import webdriver 2 import time 3 import traceback 4 from util.datetime_util import * 5 from util.find_element_util import * 6 from util.ini_parser import * 7 from util.log_util import * 8 9 10 DRIVER = "" 11 12 13 # 初始化瀏覽器 14 def init_browser(browser_name): 15 global DRIVER 16 if browser_name.lower() == "chrome": 17 DRIVER = webdriver.Chrome(CHROME_DRIVER) 18 elif browser_name.lower() == "firefox": 19 DRIVER = webdriver.Firefox(FIREFOX_DRIVER) 20 elif browser_name.lower() == "ie": 21 DRIVER = webdriver.Ie(IE_DRIVER) 22 else: 23 warning("瀏覽器【%s】不支持,已默認啟動chrome" % browser_name) 24 DRIVER = webdriver.Chrome(CHROME_DRIVER) 25 26 27 # 訪問指定url 28 def visit(url): 29 global DRIVER 30 DRIVER.get(url) 31 32 33 # 輸入操作 34 def input(locate_method, locate_exp, value): 35 global DRIVER 36 # 方式1:直接傳定位方式和定位表達式 37 if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext", 38 "partial link text", "css selector"]: 39 find_element(DRIVER, locate_method, locate_exp).send_keys(value) 40 # 方式2:通過ini文件的key找到value,再分割定位方式和定位表達式 41 else: 42 parser = IniParser(ELEMENT_FILE_PATH) 43 locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">")) 44 find_element(DRIVER, locate_method, locate_exp).send_keys(value) 45 46 47 # 點擊操作 48 def click(locate_method, locate_exp): 49 global DRIVER 50 # 方式1:直接傳定位方式和定位表達式 51 if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext", 52 "partial link text", "css selector"]: 53 find_element(DRIVER, locate_method, locate_exp).click() 54 # 方式2:通過ini文件的key找到value,再分割定位方式和定位表達式 55 else: 56 parser = IniParser(ELEMENT_FILE_PATH) 57 locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">")) 58 find_element(DRIVER, locate_method, locate_exp).click() 59 60 61 # 清空輸入框操作 62 def clear(locate_method, locate_exp): 63 global DRIVER 64 # 方式1:直接傳定位方式和定位表達式 65 if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext", 66 "partial link text", "css selector"]: 67 find_element(DRIVER, locate_method, locate_exp).clear() 68 # 方式2:通過ini文件的key找到value,再分割定位方式和定位表達式 69 else: 70 parser = IniParser(ELEMENT_FILE_PATH) 71 locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">")) 72 find_element(DRIVER, locate_method, locate_exp).clear() 73 74 75 # 切換frame 76 def switch_frame(locate_method, locate_exp): 77 global DRIVER 78 # 方式1:直接傳定位方式和定位表達式 79 if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext", 80 "partial link text", "css selector"]: 81 DRIVER.switch_to.frame(find_element(DRIVER, locate_method, locate_exp)) 82 # 方式2:通過ini文件的key找到value,再分割定位方式和定位表達式 83 else: 84 parser = IniParser(ELEMENT_FILE_PATH) 85 locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">")) 86 DRIVER.switch_to.frame(find_element(DRIVER, locate_method, locate_exp)) 87 88 89 # 切換主frame 90 def switch_home_frame(): 91 global DRIVER 92 DRIVER.switch_to.default_content() 93 94 95 # 斷言 96 def assert_word(keyword): 97 global DRIVER 98 assert keyword in DRIVER.page_source 99 100 101 # 休眠 102 def sleep(times): 103 time.sleep(int(times)) 104 105 106 # 關閉瀏覽器 107 def quit(): 108 global DRIVER 109 DRIVER.quit() 110 111 112 # 截圖函數 113 def take_screenshot(): 114 global DRIVER 115 # 創建當前日期目錄 116 dir = os.path.join(SCREENSHOT_PATH, get_chinese_date()) 117 if not os.path.exists(dir): 118 os.makedirs(dir) 119 # 以當前時間為文件名 120 file_name = get_chinese_time() 121 file_path = os.path.join(dir, file_name+".png") 122 try: 123 DRIVER.get_screenshot_as_file(file_path) 124 # 返回截圖文件的絕對路徑 125 return file_path 126 except: 127 error("截圖發生異常【{}】\n{}".format(file_path, traceback.format_exc())) 128 return file_path 129 130 131 if __name__ == "__main__": 132 init_browser("chrome") 133 visit("http://mail.126.com") 134 print(take_screenshot())
business_process 包
business_process 包是框架第三層“測試用例邏輯層”,該層主要是將服務層封裝好的各個業務對象,組織成測試邏輯,進行校驗。
case_process.py
- 測試用例文件的一行數據,拼接其中的操作動作、操作對象、操作值等關鍵字,形成與 page_action.py 中的函數相對應的字符串,並通過 eval() 轉成表達式以執行用例。
- 記錄該用例的測試結果,如測試執行結果、測試執行時間等。
- 如需數據驅動的用例集,則獲取數據驅動的數據源集合,循環將每組數據傳遞給用例步驟。
- 如果遇到需要參數化的值 ${變量名},則根據數據驅動的數據源,根據變量名進行參數化。
1 import traceback 2 import re 3 from util.global_var import * 4 from util.log_util import * 5 from util.datetime_util import * 6 from util.excel_util import Excel 7 from action.page_action import * 8 9 10 # 執行一條測試用例(即一行測試數據) 11 def execute_case(excel_file_path, case_data, test_data_source=None): 12 # 用例數據格式校驗 13 if not isinstance(case_data, (list, tuple)): 14 error("測試用例數據格式有誤!測試數據應為列表或元組類型!【%s】" % case_data) 15 case_data[TEST_SCRIPT_EXCEPTION_INFO_COL] = "測試用例數據格式有誤!應為列表或元組類型!【%s】" % case_data 16 case_data[TEST_SCRIPT_TEST_RESULT_COL] = "Fail" 17 # 該用例無需執行 18 if case_data[TEST_SCRIPT_IS_EXECUTE_COL].lower() == "n": 19 info("測試用例步驟【%s】無需執行" % case_data[TEST_SCRIPT_NAME_COL]) 20 return 21 # excel對象初始化 22 if isinstance(excel_file_path, Excel): 23 excel = excel_file_path # 如果傳入的是excel對象,則直接使用 24 else: 25 excel = Excel(excel_file_path) # 如果傳入的是文件路徑,則初始化excel對象 26 # 獲取各關鍵字 27 operation_action = case_data[TEST_SCRIPT_ACTION_COL] # 操作動作(即函數名) 28 locate_method = case_data[TEST_SCRIPT_LOCATE_METHOD_COL] # 定位方式 29 locate_expression = case_data[TEST_SCRIPT_LOCATE_EXPRESSION_COL] # 定位表達式 30 operation_value = case_data[TEST_SCRIPT_VALUE_COL] # 操作值 31 # 由於數據驅動,需要進行參數化的值 32 if test_data_source: 33 if re.search(r"\$\{\w+\}", str(operation_value)): 34 # 取出需要參數化的值 35 key = re.search(r"\$\{(\w+)\}", str(operation_value)).group(1) 36 operation_value = re.sub(r"\$\{\w+\}", str(test_data_source[key]), str(operation_value)) 37 # 將參數化后的值回寫excel測試結果中,便於回溯 38 case_data[TEST_SCRIPT_VALUE_COL] = operation_value 39 # 拼接關鍵字函數 40 if locate_method and locate_expression: 41 if operation_value: 42 func = "%s('%s', '%s', '%s')" % (operation_action, locate_method, locate_expression, operation_value) 43 else: 44 func = "%s('%s', '%s')" % (operation_action, locate_method, locate_expression) 45 else: 46 if operation_value: 47 func = "%s('%s')" % (operation_action, operation_value) 48 else: 49 func = "%s()" % operation_action 50 # 執行用例 51 try: 52 eval(func) 53 info("測試用例步驟執行成功:【{}】 {}".format(case_data[TEST_SCRIPT_NAME_COL], func)) 54 case_data[TEST_SCRIPT_TEST_RESULT_COL] = "Pass" 55 except: 56 info("測試用例步驟執行失敗:【{}】 {}".format(case_data[TEST_SCRIPT_NAME_COL], func)) 57 case_data[TEST_SCRIPT_TEST_RESULT_COL] = "Fail" 58 error(traceback.format_exc()) 59 # 進行截圖 60 case_data[TEST_SCRIPT_SCREENSHOT_PATH_COL] = take_screenshot() 61 # 異常信息記錄 62 case_data[TEST_SCRIPT_EXCEPTION_INFO_COL] = traceback.format_exc() 63 # 測試時間記錄 64 case_data[TEST_SCRIPT_TEST_TIME_COL] = get_english_datetime() 65 return case_data 66 67 68 if __name__ == "__main__": 69 excel = Excel(TEST_DATA_FILE_PATH) 70 excel.get_sheet("登錄(調試用)") 71 all_data = excel.get_all_row_data() 72 for data in all_data[1:]: 73 execute_case(excel, data)
data_source_process.py
本模塊實現了獲取數據驅動所需的數據源集合。
- 根據數據源 sheet 名,獲取該 sheet 所有行數據,每行數據作為一組測試數據。
- 每行數據作為一個字典,存儲在一個列表中。如 [{"登錄用戶名": "xxx", "登錄密碼": "xxx", ...}, {...}, ...]
1 from util.excel_util import Excel 2 from util.global_var import * 3 from util.log_util import * 4 5 6 # 數據驅動 7 # 每行數據作為一個字典,存儲在一個列表中。如[{"登錄用戶名": "xxx", "登錄密碼": "xxx", ...}, {...}, ...] 8 def get_test_data(excel_file_path, sheet_name): 9 # excel對象初始化 10 if isinstance(excel_file_path, Excel): 11 excel = excel_file_path 12 else: 13 excel = Excel(excel_file_path) 14 # 校驗sheet名 15 if not excel.get_sheet(sheet_name): 16 error("sheet【】不存在,停止執行!" % sheet_name) 17 return 18 result_list = [] 19 all_row_data = excel.get_all_row_data() 20 if len(all_row_data) <= 1: 21 error("sheet【】數據不大於1行,停止執行!" % sheet_name) 22 return 23 # 將參數化的測試數據存入全局字典 24 head_line_data = all_row_data[0] 25 for data in all_row_data[1:]: 26 if data[-1].lower() == "n": 27 continue 28 row_dict = {} 29 # 最后一列為“是否執行”列,無需取值 30 for i in range(len(data[:-1])): 31 row_dict[head_line_data[i]] = data[i] 32 result_list.append(row_dict) 33 return result_list 34 35 36 if __name__ == "__main__": 37 from util.global_var import * 38 print(get_test_data(TEST_DATA_FILE_PATH, "搜索詞")) 39 # [{'搜索詞': 'python', '斷言詞': 'python'}, {'搜索詞': 'mysql', '斷言詞': 'mysql5.6'}]
main_process.py
本模塊基於 case_process.py 和 data_source_process.py,實現關鍵字驅動+數據驅動的測試用例集的執行。
- suite_process():執行具體的測試用例步驟 sheet(如“登錄”sheet、“添加聯系人”sheet 等)
- main_suite_process():執行“測試用例”主 sheet 的用例集。每行用例集對應一個用例步驟 sheet 和數據源 sheet。
1 from util.excel_util import * 2 from util.datetime_util import * 3 from util.log_util import * 4 from util.global_var import * 5 from business_process.case_process import execute_case 6 from business_process.data_source_process import get_test_data 7 8 9 # 執行具體模塊的用例sheet(登錄sheet,添加聯系人sheet等) 10 def suite_process(excel_file_path, sheet_name, test_data_source=None): 11 """ 12 :param excel_file_path: excel文件絕對路徑或excel對象 13 :param sheet_name: 測試步驟sheet名 14 :param test_data_source: 數據驅動的數據源,默認沒有 15 :return: 16 """ 17 # 記錄測試結果統計 18 global TOTAL_CASE 19 global PASS_CASE 20 global FAIL_CASE 21 # 整個用例sheet的測試結果,默認為全部通過 22 suite_test_result = True 23 # excel對象初始化 24 if isinstance(excel_file_path, Excel): 25 excel = excel_file_path 26 else: 27 excel = Excel(excel_file_path) 28 if not excel.get_sheet(sheet_name): 29 error("sheet【%s】不存在,停止執行!" % sheet_name) 30 return 31 # 獲取測試用例集sheet的全部行數據 32 all_row_data = excel.get_all_row_data() 33 if len(all_row_data) <= 1: 34 error("sheet【%s】數據不大於1行,停止執行!" % sheet_name) 35 return 36 # 標題行數據 37 head_line_data = all_row_data[0] 38 # 切換到測試結果明細sheet,准備寫入測試結果 39 if not excel.get_sheet("測試結果明細"): 40 error("【測試結果明細】sheet不存在,停止執行!") 41 return 42 excel.write_row_data(head_line_data, None, True, "green") 43 # 執行每行的測試用例 44 for row_data in all_row_data[1:]: 45 result_data = execute_case(excel, row_data, test_data_source) 46 # 無需執行的測試步驟,跳過 47 if result_data is None: 48 continue 49 TOTAL_CASE += 1 50 if result_data[TEST_SCRIPT_TEST_RESULT_COL].lower() == "fail": 51 suite_test_result = False 52 FAIL_CASE += 1 53 else: 54 PASS_CASE += 1 55 excel.write_row_data(result_data) 56 # 切換到測試結果統計sheet,寫入統計數據 57 if not excel.get_sheet("測試結果統計"): 58 error("【測試結果統計】sheet不存在,停止執行!") 59 return 60 excel.insert_row_data(1, [TOTAL_CASE, PASS_CASE, FAIL_CASE]) 61 return excel, suite_test_result 62 63 64 # 執行【測試用例集】主sheet的用例集 65 def main_suite_process(excel_file_path, sheet_name): 66 # 初始化excel對象 67 excel = Excel(excel_file_path) 68 if not excel: 69 error("excel數據文件【%s】不存在!" % excel_file_path) 70 return 71 if not excel.get_sheet(sheet_name): 72 error("sheet名稱【%s】不存在!" % sheet_name) 73 return 74 # 獲取所有行數據 75 all_row_datas = excel.get_all_row_data() 76 if len(all_row_datas) <= 1: 77 error("sheet【%s】數據不大於1行,停止執行!" % sheet_name) 78 return 79 # 標題行數據 80 head_line_data = all_row_datas[0] 81 for row_data in all_row_datas[1:]: 82 # 校驗用例步驟sheet名是否存在 83 if row_data[MAIN_CASE_SCRIPT_SHEET_COL] not in excel.get_all_sheet(): 84 error("#" * 50 + " 用例步驟集【%s】不存在! " % row_data[MAIN_CASE_SCRIPT_SHEET_COL] + "#" * 50 + "\n") 85 row_data[MAIN_CASE_TEST_RESULT_COL] = "Fail" 86 excel.write_row_data(head_line_data, None, True, "red") 87 excel.write_row_data(row_data) 88 continue 89 # 跳過不需要執行的測試用例集 90 if row_data[MAIN_CASE_IS_EXECUTE_COL].lower() == "n": 91 info("#" * 50 + " 測試用例集【%s】無需執行!" % row_data[MAIN_CASE_CASE_NAME_COL] + "#" * 50 + "\n") 92 continue 93 # 記錄本用例集的測試時間 94 row_data[MAIN_CASE_TEST_TIME_COL] = get_english_datetime() 95 # 判斷本測試用例集是否進行數據驅動 96 if row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL]: 97 # 校驗測試數據集sheet名是否存在 98 if row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL] not in excel.get_all_sheet(): 99 error("#" * 50 + " 測試數據集【%s】不存在! " % row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL] + "#" * 50 + "\n") 100 row_data[MAIN_CASE_TEST_RESULT_COL] = "Fail" 101 excel.write_row_data(head_line_data, None, True, "red") 102 excel.write_row_data(row_data) 103 continue 104 # 獲取測試數據集 105 test_data_source = get_test_data(excel, row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL]) 106 # 每條數據進行一次本用例集的測試 107 for data_source in test_data_source: 108 info("-" * 50 + " 測試用例集【%s】開始執行!" % row_data[MAIN_CASE_CASE_NAME_COL] + "-" * 50) 109 excel, test_result_flag = suite_process(excel, row_data[MAIN_CASE_SCRIPT_SHEET_COL], data_source) 110 # 記錄本用例集的測試結果 111 if test_result_flag: 112 info("#" * 50 + " 測試用例集【%s】執行成功! " % row_data[MAIN_CASE_CASE_NAME_COL] + "#" * 50 + "\n") 113 row_data[MAIN_CASE_TEST_RESULT_COL] = "Pass" 114 else: 115 error("#" * 50 + " 測試用例集【%s】執行失敗! " % row_data[MAIN_CASE_CASE_NAME_COL] + "#" * 50 + "\n") 116 row_data[MAIN_CASE_TEST_RESULT_COL] = "Fail" 117 # 全部測試步驟結果寫入后,最后寫入本用例集的標題行和測試結果行數據 118 # 切換到“測試結果明細”sheet,以寫入測試執行結果 119 excel.get_sheet("測試結果明細") 120 excel.write_row_data(head_line_data, None, True, "red") 121 excel.write_row_data(row_data) 122 # 本用例集無需數據驅動 123 else: 124 info("-" * 50 + " 測試用例集【%s】開始執行!" % row_data[MAIN_CASE_CASE_NAME_COL] + "-" * 50) 125 excel, test_result_flag = suite_process(excel, row_data[MAIN_CASE_SCRIPT_SHEET_COL]) 126 # 記錄本用例集的測試結果 127 if test_result_flag: 128 info("#" * 50 + " 測試用例集【%s】執行成功! " % row_data[MAIN_CASE_SCRIPT_SHEET_COL] + "#" * 50 + "\n") 129 row_data[MAIN_CASE_TEST_RESULT_COL] = "Pass" 130 else: 131 error("#" * 50 + " 測試用例集【%s】執行失敗! " % row_data[MAIN_CASE_SCRIPT_SHEET_COL] + "#" * 50 + "\n") 132 row_data[MAIN_CASE_TEST_RESULT_COL] = "Fail" 133 # 全部測試步驟結果寫入后,最后寫入本用例集的標題行和測試結果行數據 134 # 切換到“測試結果明細”sheet,以寫入測試執行結果 135 excel.get_sheet("測試結果明細") 136 excel.write_row_data(head_line_data, None, True, "red") 137 excel.write_row_data(row_data) 138 return excel 139 140 141 if __name__ == "__main__": 142 from util.report_util import create_excel_report_and_send_email 143 # excel, _ = suite_process(TEST_DATA_FILE_PATH_1, "登錄1") 144 excel = main_suite_process(TEST_DATA_FILE_PATH, "測試用例集") 145 create_excel_report_and_send_email(excel, "182230124@qq.com", "UI自動化測試", "請查收附件:UI自動化測試報告")
util 包
util 包屬於第一層的測試工具層:用於實現測試過程中調用的工具類方法,例如讀取配置文件、頁面元素的操作方法、操作 Excel 文件、生成測試報告、發送郵件等。
global_var.py
本模塊用於定義測試過程中所需的全局變量。
1 import os 2 3 4 # 工程根路徑 5 PROJECT_ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 7 # 元素定位方法的ini配置文件路徑 8 ELEMENT_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "ElementsRepository.ini") 9 10 # excel文件路徑 11 TEST_DATA_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "test_data", "test_case.xlsx") 12 13 # 驅動路徑 14 CHROME_DRIVER = "E:\\auto_test_driver\\chromedriver.exe" 15 IE_DRIVER = "E:\\auto_test_driver\\IEDriverServer.exe" 16 FIREFOX_DRIVER = "E:\\auto_test_driver\\geckodriver.exe" 17 18 # 截圖路徑 19 SCREENSHOT_PATH = os.path.join(PROJECT_ROOT_PATH, "exception_pic") 20 21 # 日志配置文件路徑 22 LOG_CONF_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "Logger.conf") 23 24 # 測試報告存放路徑 25 TEST_REPORT_FILE_DIR = os.path.join(PROJECT_ROOT_PATH, "test_report") 26 27 # 對應excel測試數據文件中具體模塊sheet中的列號 28 TEST_SCRIPT_NAME_COL = 1 29 TEST_SCRIPT_ACTION_COL = 2 30 TEST_SCRIPT_LOCATE_METHOD_COL = 3 31 TEST_SCRIPT_LOCATE_EXPRESSION_COL = 4 32 TEST_SCRIPT_VALUE_COL = 5 33 TEST_SCRIPT_IS_EXECUTE_COL = 6 34 TEST_SCRIPT_TEST_TIME_COL = 7 35 TEST_SCRIPT_TEST_RESULT_COL = 8 36 TEST_SCRIPT_EXCEPTION_INFO_COL = 9 37 TEST_SCRIPT_SCREENSHOT_PATH_COL = 10 38 39 # 對應excel測試數據文件中“測試用例集”sheet列號 40 MAIN_CASE_CASE_NAME_COL = 3 41 MAIN_CASE_BROWSER_NAME_COL = 5 42 MAIN_CASE_SCRIPT_SHEET_COL = 6 43 MAIN_CASE_DATA_SOURCE_SHEET_COL = 7 44 MAIN_CASE_IS_EXECUTE_COL = 8 45 MAIN_CASE_TEST_TIME_COL = 9 46 MAIN_CASE_TEST_RESULT_COL = 10 47 48 # 測試結果統計 49 TOTAL_CASE = 0 50 PASS_CASE = 0 51 FAIL_CASE = 0 52 53 54 if __name__ == "__main__": 55 print(PROJECT_ROOT_PATH)
find_element_util.py
本模塊封裝了基於顯式等待的界面元素定位方法。
1 from selenium.webdriver.support.ui import WebDriverWait 2 3 4 # 顯式等待一個元素 5 def find_element(driver, locate_method, locate_exp): 6 # 顯式等待對象(最多等10秒,每0.2秒判斷一次等待的條件) 7 return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_element(locate_method, locate_exp)) 8 9 # 顯式等待一組元素 10 def find_elements(driver, locate_method, locate_exp): 11 # 顯式等待對象(最多等10秒,每0.2秒判斷一次等待的條件) 12 return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_elements(locate_method, locate_exp))
excel_util.py
本模塊封裝了對 excel 的讀寫操作(openpyxl 版本:3.0.4)。
1 import os 2 from openpyxl import load_workbook 3 from openpyxl.styles import PatternFill, Font, Side, Border 4 from util.datetime_util import * 5 from util.global_var import * 6 from util.log_util import * 7 8 9 # 支持excel讀寫操作的工具類 10 class Excel: 11 12 # 初始化讀取excel文件 13 def __init__(self, file_path): 14 if not os.path.exists(file_path): 15 return 16 self.wb = load_workbook(file_path) 17 # 初始化默認sheet 18 self.ws = self.wb.active 19 self.data_file_path = file_path 20 # 初始化顏色字典,供設置樣式用 21 self.color_dict = {"red": "FFFF3030", "green": "FF008B00"} 22 23 def get_all_sheet(self): 24 return self.wb.get_sheet_names() 25 26 # 打開指定sheet 27 def get_sheet(self, sheet_name): 28 if sheet_name not in self.get_all_sheet(): 29 error("sheet名稱【%s】不存在!" % sheet_name) 30 return 31 self.ws = self.wb.get_sheet_by_name(sheet_name) 32 return True 33 34 # 獲取最大行號 35 def get_max_row_no(self): 36 # openpyxl的API的行、列索引默認都從1開始 37 return self.ws.max_row 38 39 # 獲取最大列號 40 def get_max_col_no(self): 41 return self.ws.max_column 42 43 # 獲取所有行數據 44 def get_all_row_data(self, head_line=True): 45 # 是否需要標題行數據的標識,默認需要 46 if head_line: 47 min_row = 1 # 行號從1開始,即1為標題行 48 else: 49 min_row = 2 50 result = [] 51 # min_row=None:默認獲取標題行數據 52 for row in self.ws.iter_rows(min_row=min_row, max_row=self.get_max_row_no(), max_col=self.get_max_col_no()): 53 result.append([cell.value for cell in row]) 54 return result 55 56 # 獲取指定行數據 57 def get_row_data(self, row_num): 58 # 0 為標題行 59 return [cell.value for cell in self.ws[row_num+1]] 60 61 # 獲取指定列數據 62 def get_col_data(self, col_num): 63 # 索引從0開始 64 return [cell.value for cell in tuple(self.ws.columns)[col_num]] 65 66 # 追加行數據且可以設置樣式 67 def write_row_data(self, data, font_color=None, border=True, fill_color=None): 68 if not isinstance(data, (list, tuple)): 69 print("寫入數據失敗:數據不為列表或元組類型!【%s】" % data) 70 self.ws.append(data) 71 # 設置字體顏色 72 if font_color: 73 if font_color.lower() in self.color_dict.keys(): 74 font_color = self.color_dict[font_color] 75 # 設置單元格填充顏色 76 if fill_color: 77 if fill_color.lower() in self.color_dict.keys(): 78 fill_color = self.color_dict[fill_color] 79 # 設置單元格邊框 80 if border: 81 bd = Side(style="thin", color="000000") 82 # 記錄數據長度(否則會默認與之前行最長數據行的長度相同,導致樣式超過了該行實際長度) 83 count = 0 84 for cell in self.ws[self.get_max_row_no()]: 85 # 設置完該行的實際數據長度樣式后,則退出 86 if count > len(data) - 1: 87 break 88 if font_color: 89 cell.font = Font(color=font_color) 90 # 如果沒有設置字體顏色,則默認給執行結果添加字體顏色 91 else: 92 if cell.value is not None and isinstance(cell.value, str): 93 if cell.value.lower() == "pass" or cell.value == "成功": 94 cell.font = Font(color=self.color_dict["green"]) 95 elif cell.value.lower() == "fail" or cell.value == "失敗": 96 cell.font = Font(color=self.color_dict["red"]) 97 if border: 98 cell.border = Border(left=bd, right=bd, top=bd, bottom=bd) 99 if fill_color: 100 cell.fill = PatternFill(fill_type="solid", fgColor=fill_color) 101 count += 1 102 103 # 指定行插入數據(行索引從0開始) 104 def insert_row_data(self, row_no, data, font_color=None, border=True, fill_color=None): 105 if not isinstance(data, (list, tuple)): 106 print("寫入數據失敗:數據不為列表或元組類型!【%s】" % data) 107 for idx, cell in enumerate(self.ws[row_no+1]): # 此處行索引從1開始 108 cell.value = data[idx] 109 110 # 生成寫入了測試結果的excel數據文件 111 def save(self, save_file_name, timestamp): 112 save_dir = os.path.join(TEST_REPORT_FILE_DIR, get_chinese_date()) 113 if not os.path.exists(save_dir): 114 os.mkdir(save_dir) 115 save_file = os.path.join(save_dir, save_file_name + "_" + timestamp + ".xlsx") 116 self.wb.save(save_file) 117 info("生成測試結果文件:%s" % save_file) 118 return save_file 119 120 121 if __name__ == "__main__": 122 from util.global_var import * 123 from util.datetime_util import * 124 excel = Excel(TEST_DATA_FILE_PATH) 125 excel.get_sheet("測試結果統計") 126 # print(excel.get_all_row_data()) 127 # print(excel.get_row_data(1)) 128 # print(excel.get_col_data(1)) 129 # excel.write_row_data(["4", None, "嘻哈"], "green", True, "red") 130 excel.insert_row_data(1, [1,2,3]) 131 excel.save(get_timestamp())
ini_reader.py
本模塊封裝了對 ini 配置文件的讀取操作。
1 import configparser 2 3 4 class IniParser: 5 6 # 初始化打開指定ini文件並指定編碼 7 def __init__(self, file_path): 8 self.cf = configparser.ConfigParser() 9 self.cf.read(file_path, encoding="utf-8") 10 11 # 獲取所有分組名稱 12 def get_sections(self): 13 return self.cf.sections() 14 15 # 獲取指定分組的所有鍵 16 def get_options(self, section): 17 return self.cf.options(section) 18 19 # 獲取指定分組的鍵值對 20 def get_items(self, section): 21 return self.cf.items(section) 22 23 # 獲取指定分組的指定鍵的值 24 def get_value(self, section, key): 25 return self.cf.get(section, key) 26 27 28 if __name__ == "__main__": 29 from conf.global_var import * 30 parser = IniParser(ELEMENT_FILE_PATH) 31 print(parser.get_sections()) 32 print(parser.get_options("126mail_indexPage")) 33 print(parser.get_value("126mail_indexPage", 'indexpage.frame'))
email_util.py
本模塊封裝了郵件發送功能。(示例代碼中的用戶名/密碼已隱藏)
1 import yagmail 2 import traceback 3 from util.log_util import * 4 5 6 def send_mail(attachments_report_name, receiver, subject, content): 7 try: 8 # 連接郵箱服務器 9 # 注意:若使用QQ郵箱,則password為授權碼而非郵箱密碼;使用其它郵箱則為郵箱密碼 10 # encoding設置為GBK,否則中文附件名會亂碼 11 yag = yagmail.SMTP(user="******@163.com", password="******", host="smtp.163.com", encoding='GBK') 12 13 # 收件人、標題、正文、附件(若多個收件人或多個附件,則可使用列表) 14 yag.send(to=receiver, subject=subject, contents=content, attachments=attachments_report_name) 15 16 # 可簡寫:yag.send("****@163.com", subject, contents, report) 17 18 info("測試報告郵件發送成功!【郵件標題:%s】【郵件附件:%s】【收件人:%s】" % (subject, attachments_report_name, receiver)) 19 except: 20 error("測試報告郵件發送失敗!【郵件標題:%s】【郵件附件:%s】【收件人:%s】" % (subject, attachments_report_name, receiver)) 21 error(traceback.format_exc()) 22 23 24 if __name__ == "__main__": 25 send_mail("e:\\code.txt", "182230124@qq.com", "測試郵件", "正文")
datetime_util.py
該模塊實現了獲取各種格式的當前日期時間。
1 import time 2 3 4 # 返回中文格式的日期:xxxx年xx月xx日 5 def get_chinese_date(): 6 year = time.localtime().tm_year 7 if len(str(year)) == 1: 8 year = "0" + str(year) 9 month = time.localtime().tm_mon 10 if len(str(month)) == 1: 11 month = "0" + str(month) 12 day = time.localtime().tm_mday 13 if len(str(day)) == 1: 14 day = "0" + str(day) 15 return "{}年{}月{}日".format(year, month, day) 16 17 18 # 返回英文格式的日期:xxxx/xx/xx 19 def get_english_date(): 20 year = time.localtime().tm_year 21 if len(str(year)) == 1: 22 year = "0" + str(year) 23 month = time.localtime().tm_mon 24 if len(str(month)) == 1: 25 month = "0" + str(month) 26 day = time.localtime().tm_mday 27 if len(str(day)) == 1: 28 day = "0" + str(day) 29 return "{}/{}/{}".format(year, month, day) 30 31 32 # 返回中文格式的時間:xx時xx分xx秒 33 def get_chinese_time(): 34 hour = time.localtime().tm_hour 35 if len(str(hour)) == 1: 36 hour = "0" + str(hour) 37 minute = time.localtime().tm_min 38 if len(str(minute)) == 1: 39 minute = "0" + str(minute) 40 second = time.localtime().tm_sec 41 if len(str(second)) == 1: 42 second = "0" + str(second) 43 return "{}時{}分{}秒".format(hour, minute, second) 44 45 46 # 返回英文格式的時間:xx:xx:xx 47 def get_english_time(): 48 hour = time.localtime().tm_hour 49 if len(str(hour)) == 1: 50 hour = "0" + str(hour) 51 minute = time.localtime().tm_min 52 if len(str(minute)) == 1: 53 minute = "0" + str(minute) 54 second = time.localtime().tm_sec 55 if len(str(second)) == 1: 56 second = "0" + str(second) 57 return "{}:{}:{}".format(hour, minute, second) 58 59 60 # 返回中文格式的日期時間 61 def get_chinese_datetime(): 62 return get_chinese_date() + " " + get_chinese_time() 63 64 65 # 返回英文格式的日期時間 66 def get_english_datetime(): 67 return get_english_date() + " " + get_english_time() 68 69 70 # 返回時間戳 71 def get_timestamp(): 72 year = time.localtime().tm_year 73 if len(str(year)) == 1: 74 year = "0" + str(year) 75 month = time.localtime().tm_mon 76 if len(str(month)) == 1: 77 month = "0" + str(month) 78 day = time.localtime().tm_mday 79 if len(str(day)) == 1: 80 day = "0" + str(day) 81 hour = time.localtime().tm_hour 82 if len(str(hour)) == 1: 83 hour = "0" + str(hour) 84 minute = time.localtime().tm_min 85 if len(str(minute)) == 1: 86 minute = "0" + str(minute) 87 second = time.localtime().tm_sec 88 if len(str(second)) == 1: 89 second = "0" + str(second) 90 return "{}{}{}_{}{}{}".format(year, month, day, hour, minute, second) 91 92 93 if __name__ == "__main__": 94 print(get_chinese_datetime()) 95 print(get_english_datetime())
log_util.py
該模塊封裝了日志打印輸出、級別設定等功能。
1 import logging 2 import logging.config 3 from conf.global_var import * 4 5 6 # 日志配置文件:多個logger,每個logger指定不同的handler 7 # handler:設定了日志輸出行的格式 8 # 以及設定寫日志到文件(是否回滾)?還是到屏幕 9 # 還定了打印日志的級別 10 logging.config.fileConfig(LOG_CONF_FILE_PATH) 11 logger = logging.getLogger("example01") 12 13 14 def debug(message): 15 logging.debug(message) 16 17 18 def info(message): 19 logging.info(message) 20 21 22 def warning(message): 23 logging.warning(message) 24 25 26 def error(message): 27 logging.error(message) 28 29 30 if __name__=="__main__": 31 debug("hi") 32 info("hiphop") 33 warning("hello") 34 error("這是一個error日志")
report_util.py
生成測試結果文件並發送郵件。
1 from util.email_util import send_mail 2 from util.datetime_util import * 3 4 5 # 生成測試報告並發送郵件 6 def create_excel_report_and_send_email(excel_obj, receiver, subject, content): 7 """ 8 :param excel_obj: excel對象用於保存文件 9 :param timestamp: 用於文件命名的時間戳 10 :return: 返回excel測試報告文件名 11 """ 12 time_stamp = get_timestamp() 13 report_path = excel_obj.save(subject, time_stamp) 14 send_mail(report_path, receiver, subject+"_"+time_stamp, content)
conf 目錄
conf 目錄屬於第一層測試工具層,用於存儲各配置文件。
elements_repository.ini
該配置文件存儲了各頁面的元素對象的定位方式和定位表達式。
1 [126mail_indexPage] 2 indexPage.loginlink=xpath>//a[contains(text(),'密碼登錄')] 3 indexPage.frame=xpath>//iframe[contains(@id,'x-URS-iframe')] 4 indexPage.username=xpath>//input[@name='email'] 5 indexPage.password=xpath>//input[@name='password'] 6 indexPage.loginbutton=id>dologin 7 8 [126mail_homePage] 9 homePage.addressLink=xpath>//div[text()='通訊錄'] 10 11 [126mail_contactPersonPage] 12 contactPersonPage.createButton=xpath>//span[text()='新建聯系人'] 13 contactPersonPage.name=xpath>//a[@title='編輯詳細姓名']/preceding-sibling::div/input 14 contactPersonPage.email=xpath>//*[@id='iaddress_MAIL_wrap']//input 15 contactPersonPage.starContacts=xpath>//span[text()='設為星標聯系人']/preceding-sibling::span/b 16 contactPersonPage.phone=xpath>//*[@id='iaddress_TEL_wrap']//dd//input 17 contactPersonPage.otherinfo=xpath>//textarea 18 contactPersonPage.confirmButton=xpath>//span[.='確 定']
logger.conf
############################################### [loggers] keys=root,example01,example02 [logger_root] level=DEBUG handlers=hand01,hand02 [logger_example01] handlers=hand01,hand02 qualname=example01 propagate=0 [logger_example02] handlers=hand01,hand03 qualname=example02 propagate=0 ############################################### [handlers] keys=hand01,hand02,hand03 [handler_hand01] class=StreamHandler level=INFO formatter=form01 args=(sys.stderr,) [handler_hand02] class=FileHandler level=DEBUG formatter=form01 args=('E:\\pycharm_project_dir\\UIKeywordFramework\\log\\ui_test.log', 'a') [handler_hand03] class=handlers.RotatingFileHandler level=INFO formatter=form01 args=('E:\\pycharm_project_dir\\UIKeywordFramework\\log\\ui_test.log', 'a', 10*1024*1024, 5) ############################################### [formatters] keys=form01,form02 [formatter_form01] format=%(asctime)s [%(levelname)s] %(message)s datefmt=%Y-%m-%d %H:%M:%S [formatter_form02] format=%(name)-12s: [%(levelname)-8s] %(message)s datefmt=%Y-%m-%d %H:%M:%S
test_data 目錄
test_data 目錄用於存放測試數據文件(Excel),存儲了用例步驟、用例執行關鍵字、數據源等測試數據。
main.py
本模塊是本框架的運行主入口,屬於第四層“測試場景層”,將測試用例組織成測試場景,實現各種級別 cases 的管理,如冒煙,回歸等測試場景。
- 基於 business_process/main_process.py 中的模塊用例 sheet 執行函數或主 sheet 執行函數,組裝測試場景。
- 可直接用代碼組裝測試場景,也可根據 excel 數據文件的用例集合和用例步驟的維護來設定測試場景。
- 完成測試執行后生成測試結果文件並發送郵件。
1 from business_process.main_process import * 2 from util.report_util import * 3 4 5 # 組裝測試場景 6 # 冒煙測試 7 def smoke_test(report_name): 8 excel, _ = suite_process(TEST_DATA_FILE_PATH, "登錄(非數據驅動)") 9 excel, _ = suite_process(excel, "關閉瀏覽器") 10 # 生成測試報告並發送郵件 11 create_excel_report_and_send_email(excel, ['itsjuno@163.com', '182230124@qq.com'], report_name, "請查收附件:UI自動化測試報告") 12 13 14 # 全量測試:執行主sheet的用例集 15 def suite_test(report_name): 16 excel = main_suite_process(TEST_DATA_FILE_PATH, "測試用例集") 17 create_excel_report_and_send_email(excel, ['itsjuno@163.com', '182230124@qq.com'], report_name, "請查收附件:UI自動化測試報告") 18 19 20 if __name__ == "__main__": 21 # smoke_test("UI自動化測試報告_冒煙測試") 22 suite_test("UI自動化測試報告_全量測試")
test_report 目錄
本目錄用於存放測試結果文件。
exception_pic 目錄
本目錄用於存放失敗用例的截圖。
log 目錄
本目錄用於存放日志輸出文件(日志內容同時也會輸出到控制台)。
log/ui_test.log:
******