本框架的 github 地址:https://github.com/juno3550/InterfaceAutoTest
1. 框架結構說明
2. 框架代碼實現
1. 框架結構說明
整個測試框架分為四層,通過分層的方式,測試代碼更容易理解,維護起來較為方便。
第一層是“測試工具層”:
- util 包:用於實現測試過程中調用的工具類方法,例如讀取配置文件、接口請求方法、生成測試報告、發送郵件等。
- conf 目錄:存放配置文件。
- log 目錄:日志輸出文件。
第二層是“服務層”:相當於對測試對象的一個業務封裝。對於接口測試,是對遠程方法的一個實現;對於 UI 測試,是對頁面元素或操作的一個封裝。
- action 包:實現了對一個接口的請求,包含請求數據的參數化預處理及響應數據的關聯提取。
第三層是“測試用例邏輯層”:該層主要是將服務層封裝好的各個業務對象,組織成測試邏輯,進行校驗。
- bussiness_process 包:基於關鍵字的形式,實現具體模塊或測試用例集的測試腳本邏輯。
- test_data 目錄:Excel 測試數據文件,包含請求地址、請求方法、請求數據、關聯關鍵字等。
第四層是“測試場景層”:將測試用例組織成測試場景,實現各種級別 cases 的管理,如冒煙,回歸等測試場景。
- main.py:本框架工程的運行主入口。


2. 框架代碼實現
action 包
action 包為框架第二層“服務層”,相當於對測試對象的一個業務封裝。對於接口測試,是對遠程方法的一個實現;對於 UI 測試,是對頁面元素或操作的一個封裝。
case_action.py
本模塊實現了對一個接口的請求,包含請求數據的參數化預處理及響應數據的關聯提取。

關聯
在 excel 測試數據文件的具體模塊用例 sheet 中,添加了“關聯數據獲取”列用於存放關聯的正則表達式,如 userid=userid": (\d+) 表示從該行用例的接口返回結果中,獲取 userid 的值,獲取到 userid 的值之后把它放到全局變量 global_vars 中。
參數化及函數化
- 對於 ${unique_num1} 標識則調用函數獲取一個每次遞增的唯一數供注冊用戶名使用;對於 ${md5('密碼')} 則調用 md5() 函數實現請求數據的加密處理。
- 在后續用例的請求數據中,用標識符 ${userid} 和 ${token} 表示從關聯結果中獲取 userid 和 token 的值。
本模塊的函數說明
- data_preprocessor():對請求數據進行預處理:參數化及函數化。
- data_postprocessor():將響應數據需要關聯的參數保存進全局變量,供后續接口使用。
- execute_case():執行接口用例:請求數據預處理——>請求接口——>響應數據關聯提取。
1 import re 2 import traceback 3 import time 4 import requests 5 from util.excel_util import Excel 6 from util.request_util import http_client, md5, get_request_url, get_unique_num, assert_keyword 7 from util.datetime_util import * 8 from util.global_var import * 9 from util.log_util import * 10 11 12 # 對請求數據進行預處理:參數化、函數化 13 def data_preprocessor(data): 14 # 匹配需要調用唯一數函數的參數 15 if re.search(r"\$\{unique_\w*\}", data): 16 unique_num = get_unique_num() 17 # 從用例中獲取唯一數的變量名,供后續接口關聯使用 18 global_num_name = re.search(r"\$\{unique_(\w*)\}", data).group(1) 19 # 將調用獲取的唯一數的變量名和值存入全局變量中,供后續接口關聯使用 20 PARAM_GLOBAL_DICT[global_num_name] = unique_num 21 data = re.sub(r"\$\{unique_\w*\}", unique_num, data) 22 # 匹配需要進行關聯的參數 23 if re.search(r"\$\{\w+\}", data): 24 for var_data in re.findall(r"\$\{\w+\}", data): 25 var = re.search(r"\$\{(\w+)\}", var_data).group(1) 26 data = re.sub(r"\$\{%s\}"%var, PARAM_GLOBAL_DICT[var], data) 27 # 匹配需要進行函數化的參數 28 if re.search(r"\$\{\w+?\(.+?\)\}", data): 29 for var_data in re.findall(r"\$\{\w+?\(.+?\)\}", data): 30 func_var = re.search(r"\$\{(\w+?\(.+?\))\}", var_data).group(1) 31 func_result = eval(func_var) 32 data = re.sub(r"\$\{(\w+?\(.+?\))\}", func_result, data) 33 return data 34 35 36 # 將響應數據需要關聯的參數保存進全局變量,供后續接口使用 37 def data_postprocessor(response_data, revelant_param): 38 if not isinstance(revelant_param, str): 39 error("數據格式有誤!【%s】" % revelant_param) 40 error(traceback.format_exc()) 41 param, regx = revelant_param.split("=") 42 # none標識為該條測試數據沒有關聯數據,因此無需處理 43 if regx.lower() == "none": 44 return 45 if re.search(regx, response_data): 46 var_result = re.search(regx, response_data).group(1) 47 final_regx = re.sub(r"\(.+\)", var_result, regx) 48 info("關聯數據【%s】獲取成功!" % final_regx) 49 PARAM_GLOBAL_DICT[param] = var_result 50 return "%s" % final_regx 51 else: 52 error("關聯數據【%s】在響應數據中找不到!" % regx) 53 54 55 # 執行接口用例 56 def execute_case(data): 57 if not isinstance(data, (list, tuple)): 58 error("測試用例數據格式有誤!測試數據應為列表或元組類型!【%s】" % data) 59 data[CASE_EXCEPTION_INFO_COL_NO] = "測試用例數據格式有誤!應為列表或元組類型!【%s】" % data 60 data[CASE_TEST_RESULT_COL_NO] = "fail" 61 return data 62 # 該用例無需執行 63 if data[CASE_IS_EXECUTE_COL_NO].lower() == "n": 64 return 65 # 獲取請求地址 66 url = get_request_url(data[CASE_SERVER_COL_NO]) 67 # 獲取請求接口名稱 68 api_name = data[CASE_API_NAME_COL_NO] 69 info("*" * 40 + " 開始執行接口用例【%s】 " % api_name + "*" * 40) 70 # 獲取請求方法 71 method = data[CASE_METHOD_COL_NO] 72 # 替換測試數據 73 request_data = data[CASE_REQUEST_DATA_COL_NO] 74 info("data before process: %s" % request_data) 75 try: 76 request_data = data_preprocessor(request_data) 77 except: 78 error("請求數據【%s】預處理失敗!" % request_data) 79 error(traceback.format_exc()) 80 data[CASE_EXCEPTION_INFO_COL_NO] = "請求數據【%s】預處理失敗!\n%s" % (request_data, traceback.format_exc()) 81 data[CASE_TEST_RESULT_COL_NO] = "fail" 82 return data 83 else: 84 info("data after process: %s" % request_data) 85 # 數據回寫到測試結果 86 data[CASE_REQUEST_DATA_COL_NO] = request_data 87 # 請求接口並獲取響應數據 88 start_time = time.time() 89 response = http_client(url, api_name, method, request_data) 90 api_request_time = time.time() - start_time 91 data[CASE_TIME_COST_COL_NO] = int(api_request_time*1000) 92 if not isinstance(response, requests.Response): 93 error("接口用例【%s】返回的響應對象類型有誤!" % api_name) 94 data[CASE_TEST_RESULT_COL_NO] = "fail" 95 data[CASE_EXCEPTION_INFO_COL_NO] = "接口用例【%s】返回的響應對象類型【%s】有誤!" % (api_name, response) 96 return data 97 else: 98 info("接口用例【%s】請求成功!" % api_name) 99 data[CASE_API_NAME_COL_NO] = response.url 100 try: 101 data[CASE_RESPONSE_DATA_COL_NO] = response.text 102 info("接口響應數據:{}".format(response.text)) 103 # 進行斷言 104 assert_keyword(response, data[CASE_ASSERT_KEYWORD_COL_NO]) 105 info("接口用例【%s】斷言【%s】成功!" % (api_name, data[CASE_ASSERT_KEYWORD_COL_NO])) 106 data[CASE_TEST_RESULT_COL_NO] = "pass" 107 except: 108 error("接口用例【%s】斷言【%s】失敗!" % (api_name, data[CASE_ASSERT_KEYWORD_COL_NO])) 109 error(traceback.format_exc()) 110 data[CASE_TEST_RESULT_COL_NO] = "fail" 111 data[CASE_EXCEPTION_INFO_COL_NO] = traceback.format_exc() 112 return data 113 try: 114 if data[CASE_RELEVANT_PARAM_COL_NO]: 115 data[CASE_RELEVANT_PARAM_COL_NO] = data_postprocessor(response.text, data[CASE_RELEVANT_PARAM_COL_NO]) 116 except: 117 error("關聯數據【%s】處理失敗!" % data[CASE_RELEVANT_PARAM_COL_NO]) 118 error(traceback.format_exc()) 119 data[CASE_EXCEPTION_INFO_COL_NO] = "關聯數據【%s】處理失敗!\n%s" % \ 120 (data[CASE_RELEVANT_PARAM_COL_NO], traceback.format_exc()) 121 data[CASE_TEST_RESULT_COL_NO] = "fail" 122 return data 123 data[CASE_TEST_RESULT_COL_NO] = "pass" 124 return data 125 126 127 if __name__ == "__main__": 128 excel = Excel(EXCEL_FILE_PATH) 129 excel.get_sheet("注冊&登錄") 130 datas = excel.get_all_row_data(False) 131 for row_data in datas: 132 execute_case(row_data) 133 # excel.write_row_data(row_data) 134 # excel.save()
business_process 包
business_process 包是框架第三層“測試用例邏輯層”,該層主要是將服務層封裝好的各個業務對象,組織成測試邏輯,進行校驗。
main_process.py
- suite_process():基於 excel 測試數據文件,執行具體模塊的用例 sheet(注冊 sheet、登錄 sheet 等)。
- main_suite_process():基於 excel 測試數據文件,執行主 sheet “測試用例集”。
- main_process():根據入參區分執行模塊 sheet 或 主 sheet 的用例,並完成測試報告的生成與郵件發送。
1 from action.case_action import execute_case 2 from util.excel_util import Excel 3 from util.global_var import * 4 from util.datetime_util import * 5 from util.report_util import * 6 from util.log_util import * 7 from util.email_util import send_mail 8 9 10 # 執行具體模塊的用例sheet(注冊sheet、登錄sheet等) 11 def suite_process(excel_file_path, sheet_name): 12 # 記錄測試結果統計 13 global TOTAL_CASE 14 global PASS_CASE 15 global FAIL_CASE 16 # 只要有一條用例失敗,則本測試用例集結果算失敗 17 suite_test_flag = True 18 # 第一條接口用例的執行時間則為本測試用例集的執行時間 19 first_test_time = False 20 # 初始化excel對象 21 if isinstance(excel_file_path, Excel): 22 excel = excel_file_path 23 else: 24 excel = Excel(excel_file_path) 25 if not excel.get_sheet(sheet_name): 26 error("sheet名稱【%s】不存在!" % sheet_name) 27 return 28 # 獲取所有行數據 29 all_row_datas = excel.get_all_row_data() 30 if len(all_row_datas) <= 1: 31 error("sheet【】數據不大於1行,停止執行!" % sheet_name) 32 return 33 # 標題行數據 34 head_line = all_row_datas[0] 35 # 切換到“測試結果”sheet,以寫入測試執行結果 36 excel.get_sheet("測試結果明細") 37 # 寫入標題行 38 excel.write_row_data(head_line, None, True, "green") 39 # 遍歷執行用例 40 for data in all_row_datas[1:]: 41 # 跳過不需要執行的用例 42 if data[MAIN_SHEET_IS_EXECUTE_COL_NO].lower() == "n": 43 info("接口用例【%s】無需執行!" % data[CASE_API_NAME_COL_NO]) 44 continue 45 TOTAL_CASE += 1 46 # 記錄測試時間 47 execute_time = get_english_datetime() 48 # 用例集測試執行同步模塊sheet的第一條用例的執行時間 49 if not first_test_time: 50 first_test_time = execute_time 51 case_result_data = execute_case(data) 52 # 標識為n的用例返回None 53 if not case_result_data: 54 continue 55 if case_result_data[CASE_TEST_RESULT_COL_NO] == "fail": 56 suite_test_flag = False 57 # 獲取html測試報告的所需數據 58 if case_result_data[CASE_TEST_RESULT_COL_NO] == "pass": 59 PASS_CASE += 1 60 TEST_RESULT_FOR_REPORT.append([case_result_data[CASE_API_NAME_COL_NO], case_result_data[CASE_REQUEST_DATA_COL_NO], 61 case_result_data[CASE_RESPONSE_DATA_COL_NO], case_result_data[CASE_TIME_COST_COL_NO], 62 case_result_data[CASE_ASSERT_KEYWORD_COL_NO], "成功", case_result_data[CASE_EXCEPTION_INFO_COL_NO]]) 63 else: 64 FAIL_CASE += 1 65 TEST_RESULT_FOR_REPORT.append([case_result_data[CASE_API_NAME_COL_NO], case_result_data[CASE_REQUEST_DATA_COL_NO], 66 case_result_data[CASE_RESPONSE_DATA_COL_NO], case_result_data[CASE_TIME_COST_COL_NO], 67 case_result_data[CASE_ASSERT_KEYWORD_COL_NO], "失敗", case_result_data[CASE_EXCEPTION_INFO_COL_NO]]) 68 # 寫入excel測試結果明細 69 case_result_data[CASE_TEST_TIME_COL_NO] = execute_time 70 excel.write_row_data(case_result_data) 71 # 寫入excel測試結果統計 72 excel.get_sheet("測試結果統計") 73 excel.insert_row_data(1, [TOTAL_CASE, PASS_CASE, FAIL_CASE]) 74 # 返回excel對象,是為了在生成測試報告時再調用save()生成excel版測試結果文件,以此同步和html測試報告相同的時間戳命名 75 # suite_false_count 返回本次測試用例集的執行結果 76 # first_test_time 作為本測試用例集的執行時間 77 return excel, suite_test_flag, first_test_time, TOTAL_CASE, PASS_CASE, FAIL_CASE 78 79 80 # 執行主sheet“測試用例集” 81 def main_suite_process(excel_file_path, sheet_name): 82 # 初始化excel對象 83 excel = Excel(excel_file_path) 84 if not excel: 85 error("excel數據文件【%s】不存在!" % excel_file_path) 86 return 87 if not excel.get_sheet(sheet_name): 88 error("sheet名稱【%s】不存在!" % sheet_name) 89 return 90 # 獲取所有行數據 91 all_row_datas = excel.get_all_row_data() 92 if len(all_row_datas) <= 1: 93 error("sheet【%s】數據不大於1行,停止執行!" % sheet_name) 94 return 95 # 標題行數據 96 head_line = all_row_datas[0] 97 # 用例步驟行數據 98 for data in all_row_datas[1:]: 99 # 跳過不需要執行的測試用例集 100 if data[MAIN_SHEET_IS_EXECUTE_COL_NO].lower() == "n": 101 info("#" * 50 + " 測試用例集【%s】無需執行! " % data[MAIN_SHEET_SUITE_COL_NO] + "#" * 50 + "\n") 102 continue 103 if data[MAIN_SHEET_SUITE_COL_NO] not in excel.get_all_sheet(): 104 error("#" * 50 + " 測試用例集【%s】不存在! " % data[MAIN_SHEET_SUITE_COL_NO] + "#" * 50 + "\n") 105 continue 106 info("#" * 50 + " 測試用例集【%s】開始執行 " % data[MAIN_SHEET_SUITE_COL_NO] + "#" * 50) 107 excel, suite_test_flag, first_test_time, _, _, _ = suite_process(excel, data[MAIN_SHEET_SUITE_COL_NO]) 108 if suite_test_flag: 109 info("#" * 50 + " 測試用例集【%s】執行成功! " % data[MAIN_SHEET_SUITE_COL_NO] + "#" * 50 + "\n") 110 data[MAIN_SHEET_TEST_RESULT_COL_NO] = "pass" 111 else: 112 info("#" * 50 + " 測試用例集【%s】執行失敗! " % data[MAIN_SHEET_SUITE_COL_NO] + "#" * 50 + "\n") 113 data[MAIN_SHEET_TEST_RESULT_COL_NO] = "fail" 114 data[MAIN_SHEET_TEST_TIME_COL_NO] = first_test_time 115 # 切換到“測試結果明細”sheet,以寫入測試執行結果 116 excel.get_sheet("測試結果明細") 117 # 寫入標題行 118 excel.write_row_data(head_line, None, True, "red") 119 # 寫入測試結果 120 excel.write_row_data(data) 121 # 返回excel對象,是為了在生成測試報告時再調用save()生成excel版測試結果文件,以此同步和html測試報告相同的時間戳命名 122 return excel 123 124 125 # 區分模塊sheet與主sheet的用例執行,生成測試報告,發送測試報告郵件 126 def main_process(excel_file_path, sheet_name, excel_report_name, html_report_name, receiver, subject, content): 127 if sheet_name == "測試用例集": 128 excel_obj = main_suite_process(excel_file_path, sheet_name) 129 else: 130 excel_obj = suite_process(excel_file_path, sheet_name)[0] 131 if not isinstance(excel_obj, Excel): 132 error("測試執行失敗,已停止生成測試報告!") 133 timestamp = get_timestamp() 134 report_content_time = get_english_datetime() 135 xlsx, html = create_xlsx_and_html_report(TEST_RESULT_FOR_REPORT, excel_report_name, html_report_name, 136 excel_obj, timestamp, report_content_time, TOTAL_CASE, PASS_CASE, FAIL_CASE) 137 send_mail([xlsx, html], receiver, subject+"_"+timestamp, content) 138 139 140 if __name__ == "__main__": 141 # excel_obj = main_suite_process(EXCEL_FILE_PATH, "測試用例集") 142 # create_xlsx_and_html_report(TEST_RESULT_FOR_REPORT, "接口測試報告", excel_obj, get_timestamp()) 143 # suite_process(EXCEL_FILE_PATH, "注冊&登錄") 144 # main_suite_process(EXCEL_FILE_PATH, "測試用例集") 145 html_report_name = "接口自動化測試報告" 146 receiver = "182230124@qq.com" 147 subject = "接口自動化測試報告" 148 content = "接口自動化測試報告excel版和html版 請查收附件~" 149 main_process(EXCEL_FILE_PATH, "測試用例集", html_report_name, receiver, subject, content)
util 包
util 包屬於框架第一層“測試工具庫”:用於實現測試過程中調用的工具類方法,例如讀取配置文件、接口請求、生成測試報告、發送郵件等。
global_var.py
該模塊存放了整個框架中所用到的全局變量。
1 import os 2 3 4 # 工程根目錄 5 PROJECT_ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 7 # 維護唯一數參數的文件路徑 8 UNIQUE_NUM_FILE_PATH = os.path.join(PROJECT_ROOT_DIR, "config", "unique_num.txt") 9 10 # 維護接口服務端信息的ini文件路徑 11 INI_FILE_PATH = os.path.join(PROJECT_ROOT_DIR, "config", "server_info.ini") 12 13 # excel數據文件 14 EXCEL_FILE_PATH = os.path.join(PROJECT_ROOT_DIR, "test_data", "interface_test_case.xlsx") 15 16 # 維護一個參數化全局變量:供接口關聯使用 17 PARAM_GLOBAL_DICT = {} 18 19 # 測試報告保存目錄 20 TEST_REPORT_SAVE_PATH = os.path.join(PROJECT_ROOT_DIR, "test_report") 21 22 # excel數據文件用例數據列號 23 CASE_API_NAME_COL_NO = 1 24 CASE_SERVER_COL_NO = 2 25 CASE_METHOD_COL_NO = 3 26 CASE_REQUEST_DATA_COL_NO = 4 27 CASE_RESPONSE_DATA_COL_NO = 5 28 CASE_TIME_COST_COL_NO = 6 29 CASE_ASSERT_KEYWORD_COL_NO = 7 30 CASE_RELEVANT_PARAM_COL_NO = 8 31 CASE_IS_EXECUTE_COL_NO = 9 32 CASE_TEST_TIME_COL_NO = 10 33 CASE_TEST_RESULT_COL_NO = 11 34 CASE_EXCEPTION_INFO_COL_NO = 12 35 36 # excel主sheet的用例數據列號 37 MAIN_SHEET_SUITE_COL_NO = 1 38 MAIN_SHEET_IS_EXECUTE_COL_NO = 2 39 MAIN_SHEET_TEST_TIME_COL_NO = 3 40 MAIN_SHEET_TEST_RESULT_COL_NO = 4 41 42 # 存儲測試報告需要用的測試結果數據 43 TEST_RESULT_FOR_REPORT = [] 44 45 # 日志配置文件路徑 46 LOG_CONF_FILE_PATH = os.path.join(PROJECT_ROOT_DIR, "config", "logger.conf") 47 48 # 測試結果統計 49 TOTAL_CASE = 0 50 PASS_CASE = 0 51 FAIL_CASE = 0 52 53 54 if __name__ == "__main__": 55 print(PROJECT_ROOT_DIR) 56 print(LOG_CONF_FILE_PATH)
request_util.py
該模塊實現了對接口請求的所需工具函數,如獲取遞增唯一數(供注冊用戶名使用)、md5 加密(用於登錄密碼加密)、響應數據斷言等功能。
get_unique_num():用於獲取每次遞增的唯一數
該函數的目標是解決注冊用戶名重復的問題。
雖然可以在賦值注冊用戶名變量時,采用前綴字符串拼接隨機數的方式,但是用隨機數的方式仍然是有可能出現用戶名重復的情況。因此,可以在單獨的一個文件中維護一個數字,每次請求注冊接口之前,先讀取該文件中的數字,拼接用戶名前綴字符串。讀取完之后,再把這個數字進行加一的操作並保存,即每讀取一次這個數字之后,就做一次修改,進而保證每次拼接的用戶名都是唯一的,避免出現因為用戶名重復導致用例執行失敗的情況。
1 import requests 2 import json 3 import hashlib 4 import traceback 5 from util.global_var import * 6 from util.ini_reader import IniParser 7 from util.log_util import * 8 9 10 # 獲取遞增的唯一數參數 11 def get_unique_num(): 12 with open(UNIQUE_NUM_FILE_PATH, "r+") as f: 13 # 先從文件中獲取當前的唯一數 14 num = f.read() 15 # 將唯一數+1再寫回文件 16 f.seek(0, 0) 17 f.write(str(int(num)+1)) 18 return num 19 20 21 # MD5加密 22 def md5(string): 23 # 創建一個md5 hash對象 24 m = hashlib.md5() 25 # 對字符串進行md5加密的更新處理,需要指定編碼 26 m.update(string.encode("utf-8")) 27 # 返回十六進制加密結果 28 return m.hexdigest() 29 30 31 # 根據接口主機名稱映射獲取主機的IP和端口 32 def get_request_url(server_name): 33 p = IniParser(INI_FILE_PATH) 34 ip = p.get_value(server_name, "ip") 35 port = p.get_value(server_name, "port") 36 del p 37 return "http://%s:%s" % (ip, port) 38 39 40 # 接口請求函數 41 def http_client(url, api_name, method, data, headers=None, cookies=None): 42 # 校驗數據是否符合json格式 43 try: 44 # 字典對象轉json字符串 45 if isinstance(data, dict): 46 data = json.dumps(data) 47 elif isinstance(data, str): 48 data = json.loads(data) 49 data = json.dumps(data) 50 except: 51 error("接口【%s】json格式有誤!" % (url+"/%s/"%api_name)) 52 traceback.print_exc() 53 return traceback.format_exc() 54 if method.lower() == "post": 55 response = requests.post(url+"/%s/" % api_name, data=data, headers=headers, cookies=cookies) 56 elif method.lower() == "get": 57 response = requests.get(url+"/%s" % api_name, params=data, headers=headers, cookies=cookies) 58 else: 59 error("接口【%s】請求方法【%s】有誤!" % (url+"/%s/", method)) 60 return False 61 return response 62 63 64 # 斷言 65 def assert_keyword(response, keyword): 66 assert keyword in response.text
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失敗!" % 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 # 如果單元格為空(None),則轉成空字符串 54 result.append([cell.value for cell in row]) 55 return result 56 57 # 獲取指定行數據 58 def get_row_data(self, row_num): 59 # 0 為標題行 60 return [cell.value for cell in self.ws[row_num+1]] 61 62 # 獲取指定列數據 63 def get_col_data(self, col_num): 64 # 索引從0開始 65 return [cell.value for cell in tuple(self.ws.columns)[col_num]] 66 67 # 追加行數據且可以設置樣式 68 def write_row_data(self, data, font_color=None, border=True, fill_color=None): 69 if not isinstance(data, (list, tuple)): 70 print("寫入數據失敗:數據不為列表或元組類型!【%s】" % data) 71 self.ws.append(data) 72 # 設置字體顏色 73 if font_color: 74 if font_color.lower() in self.color_dict.keys(): 75 font_color = self.color_dict[font_color] 76 # 設置單元格填充顏色 77 if fill_color: 78 if fill_color.lower() in self.color_dict.keys(): 79 fill_color = self.color_dict[fill_color] 80 # 設置單元格邊框 81 if border: 82 bd = Side(style="thin", color="000000") 83 # 記錄數據長度(否則會默認與之前行最長數據行的長度相同,導致樣式超過了該行實際長度) 84 count = 0 85 for cell in self.ws[self.get_max_row_no()]: 86 # 設置完該行的實際數據長度樣式后,則退出 87 if count > len(data) - 1: 88 break 89 if font_color: 90 cell.font = Font(color=font_color) 91 # 如果沒有設置字體顏色,則默認給執行結果添加字體顏色 92 else: 93 if cell.value is not None and isinstance(cell.value, str): 94 if cell.value.lower() == "pass" or cell.value == "成功": 95 cell.font = Font(color=self.color_dict["green"]) 96 elif cell.value.lower() == "fail" or cell.value == "失敗": 97 cell.font = Font(color=self.color_dict["red"]) 98 if border: 99 cell.border = Border(left=bd, right=bd, top=bd, bottom=bd) 100 if fill_color: 101 cell.fill = PatternFill(fill_type="solid", fgColor=fill_color) 102 count += 1 103 104 # 指定行插入數據(行索引從0開始) 105 def insert_row_data(self, row_no, data, font_color=None, border=True, fill_color=None): 106 if not isinstance(data, (list, tuple)): 107 print("寫入數據失敗:數據不為列表或元組類型!【%s】" % data) 108 for idx, cell in enumerate(self.ws[row_no+1]): # 此處行索引從1開始 109 cell.value = data[idx] 110 111 # 保存寫入了測試結果的excel數據文件 112 def save(self, file_save_name, timestamp): 113 save_dir = os.path.join(TEST_REPORT_SAVE_PATH, get_chinese_date()) 114 if not os.path.exists(save_dir): 115 os.mkdir(save_dir) 116 excel_result_file_path = os.path.join(save_dir, file_save_name + "_" + timestamp + ".xlsx") 117 self.wb.save(excel_result_file_path) 118 return excel_result_file_path 119 120 121 if __name__ == "__main__": 122 from util.global_var import * 123 excel = Excel(EXCEL_FILE_PATH) 124 excel.get_sheet("測試用例集") 125 # print(excel.get_all_row_data()) 126 excel.write_row_data(["4", None, "失敗"], None, True, "green") 127 excel.save()
ini_reader.py
該模塊封裝對 ini 配置文件的讀取操作。
1 import configparser 2 import os 3 4 5 # 讀取ini文件的工具類 6 class IniParser: 7 8 # 初始化打開ini文件 9 def __init__(self, file_path): 10 if not os.path.exists(file_path): 11 print("ini文件【%s】不存在!" % file_path) 12 return 13 self.cf = configparser.ConfigParser() 14 self.cf.read(file_path, encoding="utf-8") 15 16 # 獲取所有分組 17 def get_sections(self): 18 return self.cf.sections() 19 20 # 獲取指定分組的所有鍵 21 def get_options(self, section): 22 return self.cf.options(section) 23 24 # 獲取指定分組的所有鍵值對 25 def get_items(self, section): 26 return self.cf.items(section) 27 28 # 獲取指定分組指定鍵的值 29 def get_value(self, section, option): 30 return self.cf.get(section, option) 31 32 33 if __name__ == "__main__": 34 from util.global_var import * 35 p = IniParser(INI_FILE_PATH) 36 print(p.get_sections()) 37 print(p.get_options("server1")) 38 print(p.get_items("server1")) 39 print(p.get_value("server1", "ip"))
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】" % (subject, receiver)) 19 except: 20 error("測試報告郵件發送失敗!【郵件標題:%s】【收件人:%s】" % (subject, 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 util.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("gloryroad") 33 warning("hello") 34 error("這是一個error日志")
report_util.py
該模塊實現了 html 及 excel 測試報告的生成。
1 from bottle import template 2 import os 3 from util.datetime_util import * 4 from util.log_util import * 5 6 7 # 生成測試報告html模板文件 8 def report_html(data, html_name, timestamp, report_content_time, total_num, pass_num, fail_num): 9 """ 10 :param data: 保存測試結果的列表對象 11 :param html_name: 報告名稱 12 """ 13 template_demo = """ 14 <!-- CSS goes in the document HEAD or added to your external stylesheet --> 15 <style type="text/css"> 16 table.hovertable { 17 font-family: verdana,arial,sans-serif; 18 font-size:10px; 19 color:#333333; 20 border-width: 1px; 21 border-color: #999999; 22 border-collapse: collapse; 23 } 24 table.hovertable th { 25 background-color:#ff6347; 26 border-width: 1px; 27 padding: 15px; 28 border-style: solid; 29 border-color: #a9c6c9; 30 } 31 table.hovertable tr { 32 background-color:#d4e3e5; 33 } 34 table.hovertable td { 35 border-width: 1px; 36 padding: 15px; 37 border-style: solid; 38 border-color: #a9c6c9; 39 } 40 </style> 41 <!-- Table goes in the document BODY --> 42 <title>html_name</title> 43 <head> 44 <meta http-equiv="content-type" content="txt/html; charset=utf-8" /> 45 <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> 46 </head> 47 <body> 48 <h2 style="color:grey">{{html_name}}</h2> 49 <div style="color:#C0C0C0">測試完成時間:{{report_content_time}}</div> 50 </br> 51 <span style="background-color:grey">total</span><span style="color:grey"> {{total_num }}  </span> 52 <span style="background-color:green">passed</span><span style="color:green"> {{ pass_num }}  </span> 53 <span style="background-color:red">failed</span><span style="color:red"> {{ fail_num }}  </span> 54 55 <div id="main" style="width: 600px;height:300px; margin-left: 10px;"></div> 56 <script> 57 // 繪制圖表。 58 echarts.init(document.getElementById('main')).setOption({ 59 series: { 60 name: '結果統計', 61 radius: '55%', 62 type: 'pie', 63 color: ['green', 'red'], 64 data: [ 65 {name: '成功用例數', value: {{pass_num}}}, 66 {name: '失敗用例數', value: {{fail_num}}}, 67 ], 68 label:{ // 餅圖圖形上的文本標簽 69 normal:{ 70 formatter: "{b} : {c} ({d}%)" 71 } 72 } 73 } 74 }); 75 </script> 76 <h3>測試用例執行結果明細</h3> 77 <table class="hovertable"> 78 <tr> 79 <th>接口 URL</th><th>請求數據</th><th>接口響應數據</th><th>接口調用耗時(ms)</th><th>斷言詞</th><th>測試結果</th><th>異常信息</th> 80 </tr> 81 % for url,request_data,response_data,test_time,assert_word,result,exception_info in items: 82 <tr onmouseover="this.style.backgroundColor='#ffff66';" onmouseout="this.style.backgroundColor='#d4e3e5';"> 83 84 <td>{{url}}</td><td>{{request_data}}</td><td>{{response_data}}</td><td>{{test_time}}</td><td>{{assert_word}}</td> 85 % if result == '失敗': 86 <td style="color:white;background-color:red">{{result}}</td> 87 <font color=white> 88 % elif result == '成功': 89 <td style="color:white;background-color:green">{{result}}</td> 90 % end 91 <td>{{exception_info}}</td> 92 </tr> 93 % end 94 </table> 95 </body> 96 """ 97 98 html = template(template_demo, items=data, total_num=total_num, pass_num=pass_num, fail_num=fail_num, 99 html_name=html_name, report_content_time=report_content_time) 100 """ 101 :param template_demo: 渲染的模板名稱(可以是字符串對象,也可以是模板文件名) 102 :param items: 保存測試結果的列表對象 103 :return: 渲染之后的模板(字符串對象) 104 """ 105 106 # 生成測試報告 107 save_dir = os.path.join(TEST_REPORT_SAVE_PATH, get_chinese_date()) 108 if not os.path.exists(save_dir): 109 os.mkdir(save_dir) 110 report_name = os.path.join(save_dir, html_name+"_"+timestamp+".html") 111 with open(report_name, 'wb') as f: 112 f.write(html.encode('utf-8')) 113 return os.path.join(save_dir, html_name+"_"+timestamp+".html") 114 115 116 # 將測試結果寫入html測試報告 117 def create_html_report(test_result_data, html_name, timestamp, report_content_time, total_num, pass_num, fail_num): 118 html_name = html_name 119 return report_html(test_result_data, html_name, timestamp, report_content_time, total_num, pass_num, fail_num) 120 121 122 # 將測試結果寫入excel數據文件 123 def create_excel_report(excel_obj, save_name, timestamp): 124 return excel_obj.save(save_name, timestamp) 125 126 127 # 同時生成兩種測試報告(html和html) 128 def create_xlsx_and_html_report(test_result_data, excel_name, html_name, excel_obj, 129 timestamp, report_content_time, total_num, pass_num, fail_num): 130 html_report_file_path = create_html_report(test_result_data, html_name, timestamp, 131 report_content_time, total_num, pass_num, fail_num) 132 excel_file_path = create_excel_report(excel_obj, excel_name, timestamp) 133 info("生成excel測試報告:{}".format(excel_file_path)) 134 info("生成html測試報告:{}".format(html_report_file_path)) 135 return excel_file_path, html_report_file_path
conf 目錄
unique_num.txt
該文件維護了一個數字,供 request_util.py 的 get_unique_num() 函數使用,該函數用於獲取當前文件中的數值,並寫入遞增 +1 的數值。
30179
server_info.ini
該配置文件維護了請求接口主機的地址信息。
[server1] ip = 39.100.104.214 port = 8080
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\\InterfaceAutoTest\\log\\interface_test.log', 'a') [handler_hand03] class=handlers.RotatingFileHandler level=INFO formatter=form01 args=('E:\\pycharm_project_dir\\InterfaceAutoTest\\log\\interface_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 目錄
該目錄用於存放 excel 測試數據文件。


main.py
main.py 模塊是本框架的運行主入口,屬於框架第四層“測試場景層”,將測試用例組織成測試場景,實現各種級別 cases 的管理,如冒煙,回歸等測試場景。
- 基於 business_process/main_process.py 中的模塊用例 sheet 執行函數或主 sheet 執行函數,組裝測試場景。
- 可直接用代碼組裝測試場景,也可根據 excel 數據文件對用例集合和用例步驟的維護來組裝測試場景。
- 完成測試執行后生成測試結果文件並發送郵件。
1 from bussiness_process.main_process import * 2 from util.global_var import * 3 from util.report_util import * 4 from util.datetime_util import * 5 from util.email_util import send_mail 6 7 8 # 冒煙場景 9 def smoke_test(report_file_name): 10 """ 11 :param report_file_name: 報告名稱與郵件標題 12 :return: 13 """ 14 excel_obj, _, _, _, _, _, = suite_process(EXCEL_FILE_PATH, "注冊&登錄") 15 excel_obj, _, _, total_case, pass_case, fail_case = suite_process(excel_obj, "查詢博文") 16 timestamp = get_timestamp() 17 report_content_time = get_english_datetime() 18 # 生成excel和html的兩份測試報告 19 excel_file, html_file = create_xlsx_and_html_report(TEST_RESULT_FOR_REPORT, report_file_name, report_file_name, 20 excel_obj, timestamp, report_content_time, total_case, pass_case, fail_case) 21 receiver = "182230124@qq.com" # 收件人 22 subject = report_file_name + "_" + timestamp # 郵件標題 23 content = "接口自動化測試報告excel版和html版 請查收附件~" # 郵件正文 24 send_mail([excel_file, html_file], receiver, subject, content) 25 26 27 # 測試用例集測試 28 def suite_test(report_file_name): 29 """ 30 :param report_file_name: 報告名稱與郵件標題 31 :return: 32 """ 33 receiver = "182230124@qq.com" # 收件人 34 subject = "接口自動化測試報告_全量測試" # 郵件標題 35 content = "接口自動化測試報告excel版和html版 請查收附件~" # 郵件正文 36 main_process(EXCEL_FILE_PATH, "測試用例集", report_file_name, report_file_name, receiver, subject, content) 37 38 39 if __name__ == "__main__": 40 # smoke_test("接口自動化測試報告_冒煙測試") 41 suite_test("接口自動化測試報告_全量測試")
運行結果:

test_report 目錄
該目錄用於存放 excel 和 html 版的測試報告。

Excel 報告


HTML 報告

log 目錄
該目錄用於存放測試執行日志文件(日志內容同時也會輸出到控制台)。
log/interface_test.log:

style="color:#C0C0C0"
