接口自動化測試框架:Python+Echarts+Bottle+Excel 數據驅動


本框架的 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 }}&nbsp&nbsp</span>
 52     <span style="background-color:green">passed</span><span style="color:green"> {{ pass_num }}&nbsp&nbsp</span>
 53     <span style="background-color:red">failed</span><span style="color:red"> {{ fail_num }}&nbsp&nbsp</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"


免責聲明!

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



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