Pytest+Yaml+Excel?接口自動化測試框架的實現(xiàn)示例
一、框架架構

二、項目目錄結構

三、框架功能說明
解決痛點:
- 通過session會話方式,解決了登錄之后cookie關聯(lián)處理
- 框架天然支持接口動態(tài)傳參、關聯(lián)靈活處理
- 支持Excel、Yaml文件格式編寫接口用例,通過簡單配置框架自動讀取并執(zhí)行
- 執(zhí)行環(huán)境一鍵切換,解決多環(huán)境相互影響問題
- 支持http/https協(xié)議各種請求、傳參類型接口
- 響應數(shù)據(jù)格式支持json、str類型的提取操作
- 斷言方式支持等于、包含、大于、小于、不等于等方
- 框架可以直接交給不懂代碼的功能測試人員使用,只需要安裝規(guī)范編寫接口用例就行
框架使用說明:
- 安裝依賴包:
pip install -r requirements.txt - 框架主入口為
run.py文件 - 編寫用例可以在
Excel或者Yaml文件里面,按照示例編寫即可,也可以在test_case目錄下通過python腳本編寫case - 斷言或者提取參數(shù)都是通過
jsonpath、正則表達式提取數(shù)據(jù) - 用例執(zhí)行時默認讀取
Excel和test_case目錄下用例
四、核心邏輯說明
工具類封裝
assert_util.py 斷言工具類封裝
def assert_result(response: Response, expected: str) -> None:
""" 斷言方法
:param response: 實際響應對象
:param expected: 預期響應內(nèi)容,從excel中或者yaml讀取、或者手動傳入
return None
"""
if expected is None:
logging.info("當前用例無斷言!")
return
if isinstance(expected, str):
expect_dict = eval(expected)
else:
expect_dict = expected
index = 0
for k, v in expect_dict.items():
# 獲取需要斷言的實際結果部分
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}個斷言數(shù)據(jù),實際結果:{actual} | 預期結果:{_v} 斷言方式:{k}')
allure_step(f'第{index}個斷言數(shù)據(jù)', f'實際結果:{actual} = 預期結果:{v}')
try:
if k == "eq": # 相等
assert actual == _v
elif k == "in": # 包含關系
assert _v in actual
elif k == "gt": # 判斷大于,值應該為數(shù)值型
assert actual > _v
elif k == "lt": # 判斷小于,值應該為數(shù)值型
assert actual < _v
elif k == "not": # 不等于,非
assert actual != _v
else:
logging.exception(f"判斷關鍵字: {k} 錯誤!")
except AssertionError:
raise AssertionError(f'第{index}個斷言失敗 -|- 斷言方式:{k} 實際結果:{actual} || 預期結果: {_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 配置信息
# 服務器器地址 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)建一個logger
logger = logging.getLogger(logger_name)
logger.setLevel(logging.INFO)
# 獲取本地時間,轉換為設置的格式
rq = time.strftime('%Y%m%d', time.localtime(time.time()))
# 設置所有日志和錯誤日志的存放路徑
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)
# 設置日志文件名
all_log_name = all_log_path + rq + '.log'
error_log_name = error_log_path + rq + '.log'
if not logger.handlers:
# 創(chuàng)建一個handler寫入所有日志
fh = logging.FileHandler(all_log_name, encoding='utf-8')
fh.setLevel(logging.INFO)
# 創(chuàng)建一個handler寫入錯誤日志
eh = logging.FileHandler(error_log_name, encoding='utf-8')
eh.setLevel(logging.ERROR)
# 創(chuàng)建一個handler輸出到控制臺
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
# 以時間-日志器名稱-日志級別-文件名-函數(shù)行號-錯誤內(nèi)容
all_log_formatter = logging.Formatter(
'[%(asctime)s] %(filename)s - %(levelname)s - %(lineno)s - %(message)s')
# 以時間-日志器名稱-日志級別-文件名-函數(shù)行號-錯誤內(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
報告目錄
執(zhí)行case后自動生成,執(zhí)行之前自動刪除
allure 數(shù)據(jù)目錄
執(zhí)行case后自動生成,執(zhí)行之前自動刪除
請求工具類
base_request.py 請求封裝工具類
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ù),轉換成可用數(shù)據(jù)發(fā)送請求
:param case: 讀取出來的每一行用例內(nèi)容
return: 響應對象
"""
log.info("開始執(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('請求響應數(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: 請求方法
:param url: 請求url
:param pk: 入?yún)㈥P鍵字, params(查詢參數(shù)類型,明文傳輸,一般在url?參數(shù)名=參數(shù)值), data(一般用于form表單類型參數(shù))
json(一般用于json類型請求參數(shù))
:param data: 參數(shù)數(shù)據(jù),默認等于None
:param file: 文件對象
:param header: 請求頭
:return: 返回res對象
"""
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可選關鍵字為params, json, data')
return res
pre_handle_utils.py 請求前置處理工具類
def pre_expr_handle(content) -> object:
"""
:param content: 原始的字符串內(nèi)容
return content: 替換表達式后的字符串
"""
if content is None:
return None
if len(content) != 0:
log.info(f"開始進行字符串替換: 替換字符串為:{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 響應對象
: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ā)送請求
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'])
# 自動以服務形式打開報告
# os.system('allure serve outputs/reports')
# 本地生成報告
os.system('allure generate outputs/reports -o outputs/html --clean')
shutil.rmtree(auto_gen_case_path)
if __name__ == '__main__':
run()
執(zhí)行記錄
allure 報告


日志記錄
[2022-01-11 22:36:04,164] base_request.py - INFO - 42 - 開始執(zhí)行用例: 正常登錄
[2022-01-11 22:36:04,165] pre_handle_utils.py - INFO - 37 - 開始進行字符串替換: 替換字符串為: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 - 處理請求前url:bank/api/login
[2022-01-11 22:36:04,165] pre_handle_utils.py - INFO - 78 - 處理請求后 url:http://localhost:8091/bank/api/login
[2022-01-11 22:36:04,165] pre_handle_utils.py - INFO - 90 - 處理請求前Data: {'password': '123456', 'userName': 'king'}
[2022-01-11 22:36:04,165] pre_handle_utils.py - INFO - 37 - 開始進行字符串替換: 替換字符串為:{'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 - 處理請求后Data: {'password': '123456', 'userName': 'king'}
[2022-01-11 22:36:04,166] pre_handle_utils.py - INFO - 100 - 處理請求前files: None
[2022-01-11 22:36:04,175] base_request.py - INFO - 53 - 請求響應數(shù)據(jù){"code":"0","message":"success","data":null}
[2022-01-11 22:36:04,176] data_handle.py - INFO - 29 - 提取響應內(nèi)容成功,提取表達式為: $.code 提取值為 0
[2022-01-11 22:36:04,176] assert_util.py - INFO - 49 - 第1個斷言數(shù)據(jù),實際結果:0 | 預期結果:0 斷言方式:eq
[2022-01-11 22:36:04,176] data_handle.py - INFO - 29 - 提取響應內(nèi)容成功,提取表達式為: $.message 提取值為 success
[2022-01-11 22:36:04,176] assert_util.py - INFO - 49 - 第2個斷言數(shù)據(jù),實際結果:success | 預期結果:success 斷言方式:eq
到此這篇關于Pytest+Yaml+Excel 接口自動化測試框架的實現(xiàn)示例的文章就介紹到這了,更多相關Pytest Yaml Excel 接口自動化 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
解決pip安裝報錯“error:microsoft visual c++ 14.0&nbs
這篇文章主要介紹了解決pip安裝報錯“error:microsoft visual c++ 14.0 or greater is required”問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
python SQLAlchemy的Mapping與Declarative詳解
這篇文章主要介紹了python SQLAlchemy的Mapping與Declarative詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-07-07

