UI 自動化測試框架:關鍵字驅動+數據驅動


本工程的 github 地址:https://github.com/juno3550/UIKeywordFramework

1. 關鍵字驅動框架簡介

2. 工程結構說明

3. 工程代碼實現

 

 

1. 關鍵字驅動框架簡介

原理及特點

  1. 關鍵字驅動測試是數據驅動測試的一種改進類型,它也被稱為表格驅動測試或者基於動作字的測試。
  2. 主要關鍵字包括三類:被操作對象(Item)、操作行為(Operation)和操作值(Value),用面向對象形式可將其表現為 Item.Operation(Value)
  3. 將測試邏輯按照這些關鍵字進行分解,形成數據文件。
  4. 用關鍵字的形式將測試邏輯封裝在數據文件中,測試工具只要能夠解釋這些關鍵字即可對其應用自動化。

優勢

  1. 執行人員可以不需要太多的技術:一旦框架建立,手工測試人員和非技術人員都可以很容易的編寫自動化測試腳本。
  2. 簡單易懂:它存在Excel表格中,沒有編碼,測試腳本容易閱讀和理解。關鍵字和操作行為這樣的手工測試用例,使它變得更容易編寫和維護。
  3. 早期介入:可以在應用未提交測試之前,就可以建立關鍵字驅動測試用例對象庫,從而減少后期工作。使用需求和其它相關文檔進行收集信息,關鍵字數據表可以建立手工測試程序。
  4. 代碼的重用性:用關鍵字的形式將測試用例及數據進行組裝並解釋執行,提高代碼的可重用性。

 

2. 工程結構說明

工程結構

整個測試框架分為四層,通過分層的方式,測試代碼更容易理解,維護起來較為方便。

第一層是“測試工具層”:

  • util 包:用於實現測試過程中調用的工具類方法,例如讀取配置文件、頁面元素的操作方法、操作 Excel 文件、生成測試報告、發送郵件等。
  • conf 包:配置文件及全局變量。
  • log 目錄:日志輸出文件。
  • exception_pic 目錄:失敗用例的截圖保存目錄。

第二層是“服務層”:相當於對測試對象的一個業務封裝。對於接口測試,是對遠程方法的一個實現;對於 UI 測試,是對頁面元素或操作的一個封裝

  • action 包:封裝具體的頁面動作,如點擊、輸入文本等。

第三層是“測試用例邏輯層”:該層主要是將服務層封裝好的各個業務對象,組織成測試邏輯,進行校驗

  • bussiness_process 包:基於關鍵字的形式,實現單條、多條用例的測試腳本邏輯。
  • test_data 目錄:Excel 數據文件,包含用例步驟、被操作對象、操作動作、操作值、測試結果等。

第四層是“測試場景層”:將測試用例組織成測試場景,實現各種級別 cases 的管理,如冒煙,回歸等測試場景

  • main.py:本框架工程的運行主入口。

框架特點

  1. 基於關鍵字測試框架,即使不懂開發技術的測試人員也可以實施自動化測試,便於在整個測試團隊中推廣和使用自動化測試技術,降低自動化測試實施的技術門檻。
  2. 使用外部測試數據文件,使用Excel管理測試用例的集合和每個測試用例的所有執行步驟,實現在一個文件中完成測試用例的維護工作。
  3. 通過定義關鍵字、操作元素的定位方式和定位表達式和操作值,就可以實現每個測試步驟的執行,可以更加靈活地實現自動化測試的需求。
  4. 基於關鍵字的方式,可以進行任意關鍵字的擴展,以滿足更加復雜的自動化測試需求。
  5. 實現定位表達式和測試代碼的分離,實現定位表達式直接在數據文件中進行維護。
  6. 框架提供日志功能,方便調試和監控自動化測試程序的執行。

 

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:

 

******


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM