和幾個朋友聊自然後出來的產物但願能幫到你們學習接口自動化測試,歡迎你們交流指出不合適的地方,源碼在文末html
執行接口消耗時間變長,代碼亂(語言學的不紮實),頻繁讀寫excel(可考慮用字典存每一個接口的實際響應,取值直接從響應字典中取出)
總體代碼結構優化未實現,致使最終測試時間變長,其餘工具單接口測試只須要39ms,該框架中使用了101ms,考慮和頻繁讀寫用例數據致使vue
名稱 | 版本 | 做用 |
---|---|---|
python | 3.7.8 | |
pytest | 6.0.1 | 底層單元測試框架,用來實現參數化,自動執行用例 |
allure-pytest | 2.8.17 | allure與pytest的插件能夠生成allure的測試報告 |
jsonpath | 0.82 | 用來進行響應斷言操做 |
loguru | 0.54 | 記錄日誌 |
PyYAML | 5.3.1 | 讀取yml/yaml格式的配置文件 |
Allure | 2.13.5 | 要生成allure測試報告必需要在本機安裝allure並配置環境變量 |
xlrd | 1.2.0 | 用來讀取excel中用例數據 |
yagmail | 0.11.224 | 測試完成後發送郵件 |
requests | 2.24.0 | 發送請求 |
運行test_api.py -> 讀取config.yaml(tools.read_config.py) -> 讀取excel用例文件(tools.read_data.py) -> test_api.py實現參數化 -> 處理是否依賴數據 ->base_requests.py發送請求 -> test_api.py斷言 -> read_data.py回寫實際響應到用例文件中(方便根據依賴提取對應的數據)python
server: test: http://127.0.0.1:8888/api/private/v1/ # 實例代碼使用的接口服務,已改成做者是本身的雲服務器部署。(後端源碼來自b站:https://www.bilibili.com/video/BV1EE411B7SU?p=10) dev: http://49.232.203.244:8888/api/private/v1/ # 實際響應jsonpath提取規則設置 response_reg: # 提取token的jsonpath表達式 token: $.data.token # 提取實際響應的斷言數據jsonpath表達式,與excel中預期結果的數據進行比對用 response: $.meta file_path: case_data: ../data/case_data.xlsx report_data: ../report/data/ report_generate: ../report/html/ report_zip: ../report/html/apiAutoTestReport.zip log_path: ../log/運行日誌{time}.log email: # 發件人郵箱 user: 123456.com # 發件人郵箱受權碼 password: ASGCSFSGS # 郵箱host host: smtp.163.com contents: 解壓apiAutoReport.zip(接口測試報告)後,請使用已安裝Live Server 插件的VsCode,打開解壓目錄下的index.html查看報告 # 收件人郵箱 addressees: ["收件人郵箱1","收件人郵箱2","收件人郵箱3"] title: 接口自動化測試報告(見附件) # 附件地址 enclosures: ["../report/html/apiAutoTestReport.zip",]
#!/usr/bin/env/python3 # -*- coding:utf-8 -*- """ @project: apiAutoTest @author: zy7y @file: base_requests.py @ide: PyCharm @time: 2020/7/31 """ from test import logger import requests class BaseRequest(object): def __init__(self): pass # 請求 def base_requests(self, method, url, parametric_key=None, data=None, file_var=None, file_path=None, header=None): """ :param method: 請求方法 :param url: 請求url :param parametric_key: 入參關鍵字, get/delete/head/options/請求使用params, post/put/patch請求可以使用json(application/json)/data :param data: 參數數據,默認等於None :param file_var: 接口中接受文件的參數關鍵字 :param file_path: 文件對象的地址, 單個文件直接放地址:/Users/zy7y/Desktop/vue.js 多個文件格式:["/Users/zy7y/Desktop/vue.js","/Users/zy7y/Desktop/jenkins.war"] :param header: 請求頭 :return: 返回json格式的響應 """ session = requests.Session() if (file_var in [None, '']) and (file_path in [None, '']): files = None else: # 文件不爲空的操做 if file_path.startswith('[') and file_path.endswith(']'): file_path_list = eval(file_path) files = [] # 多文件上傳 for file_path in file_path_list: files.append((file_var, (open(file_path, 'rb')))) else: # 單文件上傳 files = {file_var: open(file_path, 'rb')} if parametric_key == 'params': res = session.request(method=method, url=url, params=data, headers=header) elif parametric_key == 'data': res = session.request(method=method, url=url, data=data, files=files, headers=header) elif parametric_key == 'json': res = session.request(method=method, url=url, json=data, files=files, headers=header) else: raise ValueError('可選關鍵字爲:get/delete/head/options/請求使用params, post/put/patch請求可以使用json(application/json)/data') logger.info(f'請求方法:{method},請求路徑:{url}, 請求參數:{data}, 請求文件:{files}, 請求頭:{header})') return res.json()
#!/usr/bin/env/python3 # -*- coding:utf-8 -*- """ @project: apiAutoTest @author: zy7y @file: read_data.py @ide: PyCharm @time: 2020/7/31 """ import xlrd from test import logger class ReadData(object): def __init__(self, excel_path): self.excel_file = excel_path self.book = xlrd.open_workbook(self.excel_file) def get_data(self): """ :return: data_list - pytest參數化可用的數據 """ data_list = [] table = self.book.sheet_by_index(0) for norw in range(1, table.nrows): # 每行第4列 是否運行 if table.cell_value(norw, 3) == '否': continue value = table.row_values(norw) value.pop(3) # 配合將每一行轉換成元組存儲,迎合 pytest的參數化操做,如不須要能夠註釋掉 value = tuple(value) value = tuple(value) logger.info(f'{value}') data_list.append(value) return data_list
#!/usr/bin/env/python3 # -*- coding:utf-8 -*- """ @project: apiAutoTest的副本 @author: zy7y @file: save_response.py @ide: PyCharm @time: 2020/8/8 """ import json import jsonpath from test import logger class SaveResponse(object): def __init__(self): self.actual_response = {} # 保存實際響應 def save_actual_response(self, case_key, case_response): """ :param case_key:用例編號 :param case_response:對應用例編號的實際響應 :return: """ self.actual_response[case_key] = case_response logger.info(f'當前字典數據{self.actual_response}') # 讀取依賴數據 def read_depend_data(self, depend): """ :param depend: 須要依賴數據字典{"case_001":"['jsonpaht表達式1', 'jsonpaht表達式2']"} :return: """ depend_dict = {} depend = json.loads(depend) for k, v in depend.items(): # 取得依賴中對應case編號的值提取表達式 try: for value in v: # value : '$.data.id' # 取得對應用例編號的實際響應結果 actual = self.actual_response[k] # 返回依賴數據的key d_k = value.split('.')[-1] # 添加到依賴數據字典並返回 depend_dict[d_k] = jsonpath.jsonpath(actual, value)[0] except TypeError as e: logger.error(f'實際響應結果中沒法正常使用該表達式提取到任何內容,發現異常{e}') return depend_dict
#!/usr/bin/env/python3 # -*- coding:utf-8 -*- """ @project: apiAutoTest @author: zy7y @file: data_tearing.py @ide: PyCharm @time: 2020/8/10 """ import json from json import JSONDecodeError import jsonpath from test import logger class TreatingData(object): """ 處理hader/path路徑參數/請求data依賴數據代碼 """ def __init__(self): self.no_token_header = {} self.token_header = {} def treating_data(self, is_token, parameters, dependent, data, save_response_dict): # 使用那個header if is_token == '': header = self.no_token_header else: header = self.token_header logger.info(f'處理依賴前data的數據:{data}') # 處理依賴數據data if dependent != '': dependent_data = save_response_dict.read_depend_data(dependent) logger.debug(f'依賴數據解析得到的字典{dependent_data}') if data != '': # 合併組成一個新的data dependent_data.update(json.loads(data)) data = dependent_data logger.info(f'data有數據,依賴有數據時 {data}') else: # 賦值給data data = dependent_data logger.info(f'data無數據,依賴有數據時 {data}') else: if data == '': data = None logger.info(f'data無數據,依賴無數據時 {data}') else: data = json.loads(data) logger.info(f'data有數據,依賴無數據 {data}') # 處理路徑參數Path的依賴 # 傳進來的參數相似 {"case_002":"$.data.id"}/item/{"case_002":"$.meta.status"},進行列表拆分 path_list = parameters.split('/') # 獲取列表長度迭代 for i in range(len(path_list)): # 按着 try: # 嘗試序列化成dict: json.loads('2') 能夠轉換成2 path_dict = json.loads(path_list[i]) except JSONDecodeError as e: # 序列化失敗此path_list[i]的值不變化 logger.error(f'沒法轉換字典,進入下一個檢查,本輪值不發生變化:{path_list[i]},{e}') # 跳過進入下次循環 continue else: # 解析該字典,得到用例編號,表達式 logger.info(f'{path_dict}') # 處理json.loads('數字')正常序列化致使的AttributeError try: for k, v in path_dict.items(): try: # 嘗試從對應的case實際響應提取某個字段內容 path_list[i] = jsonpath.jsonpath(save_response_dict.actual_response[k], v)[0] except TypeError as e: logger.error(f'沒法提取,請檢查響應字典中是否支持該表達式,{e}') except AttributeError as e: logger.error(f'類型錯誤:{type(path_list[i])},本此將不轉換值 {path_list[i]},{e}') # 字典中存在有不是str的元素:使用map 轉換成全字符串的列表 path_list = map(str, path_list) # 將字符串列表轉換成字符:500/item/200 parameters_path_url = "/".join(path_list) logger.info(f'path路徑參數解析依賴後的路徑爲{parameters_path_url}') return data, header, parameters_path_url
#!/usr/bin/env/python3 # -*- coding:utf-8 -*- """ @project: apiAutoTest @author: zy7y @file: test_api.py @ide: PyCharm @time: 2020/7/31 """ import json import jsonpath from test import logger import pytest import allure from api.base_requests import BaseRequest from tools.data_tearing import TreatingData from tools.read_config import ReadConfig from tools.read_data import ReadData from tools.save_response import SaveResponse # 讀取配置文件 對象 rc = ReadConfig() base_url = rc.read_serve_config('dev') token_reg, res_reg = rc.read_response_reg() case_data_path = rc.read_file_path('case_data') report_data = rc.read_file_path('report_data') report_generate = rc.read_file_path('report_generate') log_path = rc.read_file_path('log_path') report_zip = rc.read_file_path('report_zip') email_setting = rc.read_email_setting() # 實例化存響應的對象 save_response_dict = SaveResponse() # 讀取excel數據對象 data_list = ReadData(case_data_path).get_data() # 數據處理對象 treat_data = TreatingData() # 請求對象 br = BaseRequest() logger.info(f'配置文件/excel數據/對象實例化,等前置條件處理完畢\n\n') class TestApiAuto(object): # 啓動方法 def run_test(self): import os, shutil if os.path.exists('../report') and os.path.exists('../log'): shutil.rmtree(path='../report') shutil.rmtree(path='../log') # 日誌存取路徑 logger.add(log_path, encoding='utf-8') pytest.main(args=[f'--alluredir={report_data}']) os.system(f'allure generate {report_data} -o {report_generate} --clean') logger.warning('報告已生成') @pytest.mark.parametrize('case_number,case_title,path,is_token,method,parametric_key,file_var,' 'file_path, parameters, dependent,data,expect', data_list) def test_main(self, case_number, case_title, path, is_token, method, parametric_key, file_var, file_path, parameters, dependent, data, expect): """ :param case_number: 用例編號 :param case_title: 用例標題 :param path: 接口路徑 :param is_token: token操做:寫入token/讀取token/不攜帶token :param method: 請求方式:get/post/put/delete.... :param parametric_key: 入參關鍵字:params/data/json :param file_var: 接口中接受文件對象的參數名稱 :param file_path: 文件路徑,單文件實例:/Users/zy7y/PycharmProjects/apiAutoTest/test/__init__.py 多文件實例['/Users/zy7y/PycharmProjects/apiAutoTest/test/__init__.py','/Users/zy7y/PycharmProjects/apiAutoTest/test/test_api.py'] :param parameters: path參數(攜帶在url中的參數)依賴處理 users/:id(id攜帶在url中) 實例:{"case_001": '$.data.id'},解析 從用例編號爲case_001的實際結果響應中提取data字典裏面的id的內容(假設提取出來是500), 最後請求的路徑將是host + users/500 :param dependent: data數據依賴,該接口須要上一個接口返回的響應中的某個字段及內容:實例{"case_001",["$.data.id","$.data.username"]} 解析: 從用例case_001的實際響應結果中提取到data下面的id,與username的值(假設id值爲500,username爲admin),那麼提取的數據依賴內容將是{"id":500, "username":"admin"} 納悶最終請求的data 將是 {"id":500, "username":"admin"} 與自己的data合併後的內容 :param data: 請求數據 :param expect:預期結果,最後與config/config.yaml下的response_reg->response提取出來的實際響應內容作對比,實現斷言 :return: """ # 感謝:https://www.cnblogs.com/yoyoketang/p/13386145.html,提供動態添加標題的實例代碼 # 動態添加標題 allure.dynamic.title(case_title) logger.debug(f'⬇️⬇️⬇️...執行用例編號:{case_number}...⬇️⬇️⬇️️') with allure.step("處理相關數據依賴,header"): data, header, parameters_path_url = treat_data.treating_data(is_token, parameters, dependent, data, save_response_dict) with allure.step("發送請求,取得響應結果的json串"): res = br.base_requests(method=method, url=base_url + path + parameters_path_url, parametric_key=parametric_key, file_var=file_var, file_path=file_path, data=data, header=header) with allure.step("將響應結果的內容寫入實際響應字典中"): save_response_dict.save_actual_response(case_key=case_number, case_response=res) # 寫token的接口必須是要正確無誤能返回token的 if is_token == '寫': with allure.step("從登陸後的響應中提取token到header中"): treat_data.token_header['Authorization'] = jsonpath.jsonpath(res, token_reg)[0] with allure.step("根據配置文件的提取響應規則提取實際數據"): really = jsonpath.jsonpath(res, res_reg)[0] with allure.step("處理讀取出來的預期結果響應"): expect = json.loads(expect) with allure.step("預期結果與實際響應進行斷言操做"): assert really == expect logger.info(f'完整的json響應: {res}\n須要校驗的數據字典: {really} 預期校驗的數據字典: {expect} \n測試結果: {really == expect}') logger.debug(f'⬆⬆⬆...用例編號:{case_number},執行完畢,日誌查看...⬆⬆⬆\n\n️') if __name__ == '__main__': TestApiAuto().run_test() # 使用jenkins集成將不會使用到這兩個方法(郵件發送/報告壓縮zip) # from tools.zip_file import zipDir # from tools.send_email import send_email # zipDir(report_generate, report_zip) # send_email(email_setting)
首先確保須要的環境與依賴包無問題以後,使用Pycharm打開項目,找到settings
修改成unitest或者其餘非pytest,具體操做以下
git
jsonpath語法學習:https://blog.csdn.net/liuchunming033/article/details/106272542
zip文件壓縮:http://www.javashuo.com/article/p-uumfypnx-dz.html
歡迎交流。github
源碼地址Gitee - version1.0分支: https://gitee.com/zy7y/apiAutoTest/tree/version1.0/
源碼地址GitHub- version1.0 分支:https://github.com/zy7y/apiAutoTest/tree/version1.0/json
2020/11/23 - 優化數據參數、路徑參數依賴處理方式,現版本與以前同等環境下,測試時間提高2S
介紹:http://www.javashuo.com/article/p-ywzmuiuw-nv.html後端