Pytest+Yaml+Excel?接口自動(dòng)化測(cè)試框架的實(shí)現(xiàn)示例
一、框架架構(gòu)
二、項(xiàng)目目錄結(jié)構(gòu)
三、框架功能說(shuō)明
解決痛點(diǎn):
- 通過(guò)session會(huì)話方式,解決了登錄之后cookie關(guān)聯(lián)處理
- 框架天然支持接口動(dòng)態(tài)傳參、關(guān)聯(lián)靈活處理
- 支持Excel、Yaml文件格式編寫接口用例,通過(guò)簡(jiǎn)單配置框架自動(dòng)讀取并執(zhí)行
- 執(zhí)行環(huán)境一鍵切換,解決多環(huán)境相互影響問(wèn)題
- 支持http/https協(xié)議各種請(qǐng)求、傳參類型接口
- 響應(yīng)數(shù)據(jù)格式支持json、str類型的提取操作
- 斷言方式支持等于、包含、大于、小于、不等于等方
- 框架可以直接交給不懂代碼的功能測(cè)試人員使用,只需要安裝規(guī)范編寫接口用例就行
框架使用說(shuō)明:
- 安裝依賴包:
pip install -r requirements.txt
- 框架主入口為
run.py
文件 - 編寫用例可以在
Excel
或者Yaml
文件里面,按照示例編寫即可,也可以在test_case
目錄下通過(guò)python腳本編寫case - 斷言或者提取參數(shù)都是通過(guò)
jsonpath
、正則表達(dá)式
提取數(shù)據(jù) - 用例執(zhí)行時(shí)默認(rèn)讀取
Excel
和test_case
目錄下用例
四、核心邏輯說(shuō)明
工具類封裝
assert_util.py 斷言工具類封裝
def assert_result(response: Response, expected: str) -> None: """ 斷言方法 :param response: 實(shí)際響應(yīng)對(duì)象 :param expected: 預(yù)期響應(yīng)內(nèi)容,從excel中或者yaml讀取、或者手動(dòng)傳入 return None """ if expected is None: logging.info("當(dāng)前用例無(wú)斷言!") return if isinstance(expected, str): expect_dict = eval(expected) else: expect_dict = expected index = 0 for k, v in expect_dict.items(): # 獲取需要斷言的實(shí)際結(jié)果部分 for _k, _v in v.items(): if _k == "http_code": actual = response.status_code else: if response_type(response) == "json": actual = json_extractor(response.json(), _k) else: actual = re_extract(response.text, _k) index += 1 logging.info(f'第{index}個(gè)斷言數(shù)據(jù),實(shí)際結(jié)果:{actual} | 預(yù)期結(jié)果:{_v} 斷言方式:{k}') allure_step(f'第{index}個(gè)斷言數(shù)據(jù)', f'實(shí)際結(jié)果:{actual} = 預(yù)期結(jié)果:{v}') try: if k == "eq": # 相等 assert actual == _v elif k == "in": # 包含關(guān)系 assert _v in actual elif k == "gt": # 判斷大于,值應(yīng)該為數(shù)值型 assert actual > _v elif k == "lt": # 判斷小于,值應(yīng)該為數(shù)值型 assert actual < _v elif k == "not": # 不等于,非 assert actual != _v else: logging.exception(f"判斷關(guān)鍵字: {k} 錯(cuò)誤!") except AssertionError: raise AssertionError(f'第{index}個(gè)斷言失敗 -|- 斷言方式:{k} 實(shí)際結(jié)果:{actual} || 預(yù)期結(jié)果: {_v}')
case_handle.py Case數(shù)據(jù)讀取工具類
def get_case_data(): case_type = ReadYaml(config_path + "config.yaml").read_yaml["case"] if case_type == CaseType.EXCEL.value: cases = [] for file in [excel for excel in os.listdir(data_path) if os.path.splitext(excel)[1] == ".xlsx"]: data = ReadExcel(data_path + file).read_excel() name = os.path.splitext(file)[0] class_name = name.split("_")[0].title() + name.split("_")[1].title() gen_case(name, data, class_name) cases.extend(data) return cases elif case_type == CaseType.YAML.value: cases = [] for yaml_file in [yaml for yaml in os.listdir(data_path) if os.path.splitext(yaml)[1] in [".yaml", "yml"]]: data = ReadYaml(data_path + yaml_file).read_yaml name = os.path.splitext(yaml_file)[0] class_name = name.split("_")[0].title() + name.split("_")[1].title() gen_case(name, data, class_name) cases.extend(data) return cases else: cases = [] for file in [excel for excel in os.listdir(data_path) if os.path.splitext(excel)[1] in [".yaml", "yml", ".xlsx"]]: if os.path.splitext(file)[1] == ".xlsx": data = ReadExcel(data_path + file).read_excel() name = os.path.splitext(file)[0] cases.extend(data) else: data = ReadYaml(data_path + file).read_yaml name = os.path.splitext(file)[0] cases.extend(data) class_name = name.split("_")[0].title() + name.split("_")[1].title() gen_case(name, data, class_name) return cases
excel_handle.py 讀取Excel工具類
def get_case_data(): case_type = ReadYaml(config_path + "config.yaml").read_yaml["case"] if case_type == CaseType.EXCEL.value: cases = [] for file in [excel for excel in os.listdir(data_path) if os.path.splitext(excel)[1] == ".xlsx"]: data = ReadExcel(data_path + file).read_excel() name = os.path.splitext(file)[0] class_name = name.split("_")[0].title() + name.split("_")[1].title() gen_case(name, data, class_name) cases.extend(data) return cases elif case_type == CaseType.YAML.value: cases = [] for yaml_file in [yaml for yaml in os.listdir(data_path) if os.path.splitext(yaml)[1] in [".yaml", "yml"]]: data = ReadYaml(data_path + yaml_file).read_yaml name = os.path.splitext(yaml_file)[0] class_name = name.split("_")[0].title() + name.split("_")[1].title() gen_case(name, data, class_name) cases.extend(data) return cases else: cases = [] for file in [excel for excel in os.listdir(data_path) if os.path.splitext(excel)[1] in [".yaml", "yml", ".xlsx"]]: if os.path.splitext(file)[1] == ".xlsx": data = ReadExcel(data_path + file).read_excel() name = os.path.splitext(file)[0] cases.extend(data) else: data = ReadYaml(data_path + file).read_yaml name = os.path.splitext(file)[0] cases.extend(data) class_name = name.split("_")[0].title() + name.split("_")[1].title() gen_case(name, data, class_name) return cases
yaml_handle.py 讀取Yaml文件的工具類
class ReadYaml: def __init__(self, filename): self.filename = filename @property def read_yaml(self) -> object: with open(file=self.filename, mode="r", encoding="utf-8") as fp: case_data = yaml.safe_load(fp.read()) return case_data
配置文件
config.yaml 配置信息
# 服務(wù)器器地址 host: http://localhost:8091/ case: 1 # 0代表執(zhí)行Excel和yaml兩種格式的用例, 1 代表Excel用例,2 代表 yaml文件用例
輸出目錄
日志輸出目錄
import logging import time import os def get_log(logger_name): """ :param logger_name: 日志名稱 :return: 返回logger handle """ # 創(chuàng)建一個(gè)logger logger = logging.getLogger(logger_name) logger.setLevel(logging.INFO) # 獲取本地時(shí)間,轉(zhuǎn)換為設(shè)置的格式 rq = time.strftime('%Y%m%d', time.localtime(time.time())) # 設(shè)置所有日志和錯(cuò)誤日志的存放路徑 path = os.path.dirname(os.path.abspath(__file__)) all_log_path = os.path.join(path, 'interface_logs\\All_Logs\\') if not os.path.exists(all_log_path): os.makedirs(all_log_path) error_log_path = os.path.join(path, 'interface_logs\\Error_Logs\\') if not os.path.exists(error_log_path): os.makedirs(error_log_path) # 設(shè)置日志文件名 all_log_name = all_log_path + rq + '.log' error_log_name = error_log_path + rq + '.log' if not logger.handlers: # 創(chuàng)建一個(gè)handler寫入所有日志 fh = logging.FileHandler(all_log_name, encoding='utf-8') fh.setLevel(logging.INFO) # 創(chuàng)建一個(gè)handler寫入錯(cuò)誤日志 eh = logging.FileHandler(error_log_name, encoding='utf-8') eh.setLevel(logging.ERROR) # 創(chuàng)建一個(gè)handler輸出到控制臺(tái) ch = logging.StreamHandler() ch.setLevel(logging.ERROR) # 以時(shí)間-日志器名稱-日志級(jí)別-文件名-函數(shù)行號(hào)-錯(cuò)誤內(nèi)容 all_log_formatter = logging.Formatter( '[%(asctime)s] %(filename)s - %(levelname)s - %(lineno)s - %(message)s') # 以時(shí)間-日志器名稱-日志級(jí)別-文件名-函數(shù)行號(hào)-錯(cuò)誤內(nèi)容 error_log_formatter = logging.Formatter( '[%(asctime)s] %(filename)s - %(levelname)s - %(lineno)s - %(message)s') # 將定義好的輸出形式添加到handler fh.setFormatter(all_log_formatter) ch.setFormatter(all_log_formatter) eh.setFormatter(error_log_formatter) # 給logger添加handler logger.addHandler(fh) logger.addHandler(eh) logger.addHandler(ch) return logger
報(bào)告目錄
執(zhí)行case后自動(dòng)生成,執(zhí)行之前自動(dòng)刪除
allure 數(shù)據(jù)目錄
執(zhí)行case后自動(dòng)生成,執(zhí)行之前自動(dòng)刪除
請(qǐng)求工具類
base_request.py 請(qǐng)求封裝工具類
class BaseRequest: session = None @classmethod def get_session(cls): if cls.session is None: cls.session = requests.Session() return cls.session @classmethod def send_request(cls, case: dict) -> Response: """ 處理case數(shù)據(jù),轉(zhuǎn)換成可用數(shù)據(jù)發(fā)送請(qǐng)求 :param case: 讀取出來(lái)的每一行用例內(nèi)容 return: 響應(yīng)對(duì)象 """ log.info("開(kāi)始執(zhí)行用例: {}".format(case.get("title"))) req_data = RequestPreDataHandle(case).to_request_data res = cls.send_api( url=req_data["url"], method=req_data["method"], pk=req_data["pk"], header=req_data.get("header", None), data=req_data.get("data", None), file=req_data.get("file", None) ) allure_step('請(qǐng)求響應(yīng)數(shù)據(jù)', res.text) after_extract(res, req_data.get("extract", None)) return res @classmethod def send_api(cls, url, method, pk, header=None, data=None, file=None) -> Response: """ :param method: 請(qǐng)求方法 :param url: 請(qǐng)求url :param pk: 入?yún)㈥P(guān)鍵字, params(查詢參數(shù)類型,明文傳輸,一般在url?參數(shù)名=參數(shù)值), data(一般用于form表單類型參數(shù)) json(一般用于json類型請(qǐng)求參數(shù)) :param data: 參數(shù)數(shù)據(jù),默認(rèn)等于None :param file: 文件對(duì)象 :param header: 請(qǐng)求頭 :return: 返回res對(duì)象 """ session = cls.get_session() pk = pk.lower() if pk == 'params': res = session.request(method=method, url=url, params=data, headers=header) elif pk == 'data': res = session.request(method=method, url=url, data=data, files=file, headers=header) elif pk == 'json': res = session.request(method=method, url=url, json=data, files=file, headers=header) else: raise ValueError('pk可選關(guān)鍵字為params, json, data') return res
pre_handle_utils.py 請(qǐng)求前置處理工具類
def pre_expr_handle(content) -> object: """ :param content: 原始的字符串內(nèi)容 return content: 替換表達(dá)式后的字符串 """ if content is None: return None if len(content) != 0: log.info(f"開(kāi)始進(jìn)行字符串替換: 替換字符串為:{content}") content = Template(str(content)).safe_substitute(GLOBAL_VARS) for func in re.findall('\\${(.*?)}', content): try: content = content.replace('${%s}' % func, exec_func(func)) except Exception as e: log.exception(e) log.info(f"字符串替換完成: 替換字符串后為:{content}") return content
after_handle_utils.py 后置操作處理工具類
def after_extract(response: Response, exp: str) -> None: """ :param response: request 響應(yīng)對(duì)象 :param exp: 需要提取的參數(shù)字典 '{"k1": "$.data"}' 或 '{"k1": "data:(.*?)$"}' :return: """ if exp: if response_type(response) == "json": res = response.json() for k, v in exp.items(): GLOBAL_VARS[k] = json_extractor(res, v) else: res = response.text for k, v in exp.items(): GLOBAL_VARS[k] = re_extract(res, v)
代碼編寫case
test_demo.py 用例文件示例
@allure.feature("登錄") class TestLogin: @allure.story("正常登錄成功") @allure.severity(allure.severity_level.BLOCKER) def test_login(self): allure_title("正常登錄") data = { "url": "api/login", "method": "post", "pk": "data", "data": {"userName": "king", "pwd": 123456} } expected = { "$.msg": "登錄成功!" } # 發(fā)送請(qǐng)求 response = BaseRequest.send_request(data) # 斷言操作 assert_result(response, expected)
程序主入口
run.py 主入口執(zhí)行文件
def run(): # 生成case在執(zhí)行 if os.path.exists(auto_gen_case_path): shutil.rmtree(auto_gen_case_path) get_case_data() if os.path.exists('outputs/reports/'): shutil.rmtree(path='outputs/reports/') # 本地調(diào)式執(zhí)行 pytest.main(args=['-s', '--alluredir=outputs/reports']) # 自動(dòng)以服務(wù)形式打開(kāi)報(bào)告 # os.system('allure serve outputs/reports') # 本地生成報(bào)告 os.system('allure generate outputs/reports -o outputs/html --clean') shutil.rmtree(auto_gen_case_path) if __name__ == '__main__': run()
執(zhí)行記錄
allure 報(bào)告
日志記錄
[2022-01-11 22:36:04,164] base_request.py - INFO - 42 - 開(kāi)始執(zhí)行用例: 正常登錄 [2022-01-11 22:36:04,165] pre_handle_utils.py - INFO - 37 - 開(kāi)始進(jìn)行字符串替換: 替換字符串為:bank/api/login [2022-01-11 22:36:04,165] pre_handle_utils.py - INFO - 44 - 字符串替換完成: 替換字符串后為:bank/api/login [2022-01-11 22:36:04,165] pre_handle_utils.py - INFO - 68 - 處理請(qǐng)求前url:bank/api/login [2022-01-11 22:36:04,165] pre_handle_utils.py - INFO - 78 - 處理請(qǐng)求后 url:http://localhost:8091/bank/api/login [2022-01-11 22:36:04,165] pre_handle_utils.py - INFO - 90 - 處理請(qǐng)求前Data: {'password': '123456', 'userName': 'king'} [2022-01-11 22:36:04,165] pre_handle_utils.py - INFO - 37 - 開(kāi)始進(jìn)行字符串替換: 替換字符串為:{'password': '123456', 'userName': 'king'} [2022-01-11 22:36:04,166] pre_handle_utils.py - INFO - 44 - 字符串替換完成: 替換字符串后為:{'password': '123456', 'userName': 'king'} [2022-01-11 22:36:04,166] pre_handle_utils.py - INFO - 92 - 處理請(qǐng)求后Data: {'password': '123456', 'userName': 'king'} [2022-01-11 22:36:04,166] pre_handle_utils.py - INFO - 100 - 處理請(qǐng)求前files: None [2022-01-11 22:36:04,175] base_request.py - INFO - 53 - 請(qǐng)求響應(yīng)數(shù)據(jù){"code":"0","message":"success","data":null} [2022-01-11 22:36:04,176] data_handle.py - INFO - 29 - 提取響應(yīng)內(nèi)容成功,提取表達(dá)式為: $.code 提取值為 0 [2022-01-11 22:36:04,176] assert_util.py - INFO - 49 - 第1個(gè)斷言數(shù)據(jù),實(shí)際結(jié)果:0 | 預(yù)期結(jié)果:0 斷言方式:eq [2022-01-11 22:36:04,176] data_handle.py - INFO - 29 - 提取響應(yīng)內(nèi)容成功,提取表達(dá)式為: $.message 提取值為 success [2022-01-11 22:36:04,176] assert_util.py - INFO - 49 - 第2個(gè)斷言數(shù)據(jù),實(shí)際結(jié)果:success | 預(yù)期結(jié)果:success 斷言方式:eq
到此這篇關(guān)于Pytest+Yaml+Excel 接口自動(dòng)化測(cè)試框架的實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)Pytest Yaml Excel 接口自動(dòng)化 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Pyqt5 關(guān)于流式布局和滾動(dòng)條的綜合使用示例代碼
這篇文章主要介紹了Pyqt5 關(guān)于流式布局和滾動(dòng)條的綜合使用示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03python3.7將代碼打包成exe程序并添加圖標(biāo)的方法
這篇文章主要介紹了python3.7將代碼打包成exe程序并添加圖標(biāo)的方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-10-10解決pip安裝報(bào)錯(cuò)“error:microsoft visual c++ 14.0&nbs
這篇文章主要介紹了解決pip安裝報(bào)錯(cuò)“error:microsoft visual c++ 14.0 or greater is required”問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05Python與Java進(jìn)行交互操作的方法與性能對(duì)比
這篇文章主要為大家詳細(xì)介紹了Python與Java進(jìn)行交互操作的相關(guān)方法,包括子進(jìn)程調(diào)用,JPype,Py4J和REST/gRPC,并進(jìn)行了性能對(duì)比,感興趣的小伙伴可以了解下2025-04-04python SQLAlchemy的Mapping與Declarative詳解
這篇文章主要介紹了python SQLAlchemy的Mapping與Declarative詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-07-07