Python介面自動化測試框架: pytest+allure+jsonpath+requests+excel實現的介面自動化測試框架(學習成果)
阿新 • • 發佈:2020-08-03
[toc]
# 廢話
最近在自己學習介面自動化測試,這裡也算是完成一個小的成果,歡迎大家交流指出不合適的地方,原始碼在文末
# 問題
整體程式碼結構優化未實現,導致最終測試時間變長,其他工具單介面測試只需要39ms,該框架中使用了101ms,考慮和頻繁讀寫用例資料導致
# 環境與依賴
| 名稱 | 版本 | 作用 |
| -------- | -------- | ---- |
| 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中用例資料 |
| xlutils | 2.0.0 | 用來向excel中寫入實際的響應結果 |
| yagmail | 0.11.224 | 測試完成後傳送郵件 |
| requests| 2.24.0 | 傳送請求 |
# 目錄結構
![35F236C2-2F64-4891-8384-2FBFE3229F90.png](http://ww1.sinaimg.cn/large/005EVOgYly1ghdn108dfij312w0tdgpj.jpg)
# 執行順序
執行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回寫實際響應到用例檔案中(方便根據依賴提取對應的資料)
# config.ymal展示
```yml
server:
# 伺服器host地址,傳送請求的url= host+ path
test: http://127.0.0.1:8888/api/private/v1/
dev: http://47.115.124.102:8888/api/private/v1/
response_reg:
# 提取token的表示式
token: $.data.token
# 提取實際響應中的某部分來作為斷言資料(例項中斷言的是meta這個子字典,預期結果也是寫的meta子字典中的內容)
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: 發件人郵箱
password: 郵箱授權碼(不是密碼)
host: smtp.163.com
contents: 解壓apiAutoReport.zip(介面測試報告)後,請使用已安裝Live Server 外掛的VsCode,開啟解壓目錄下的index.html檢視報告
# 發件人列表
addressees: ["[email protected]","[email protected]","[email protected]"]
title: 介面自動化測試報告(見附件)
# 測試報告附件
enclosures: ["../report/html/apiAutoTestReport.zip",]
```
# EXcel用例展示
![Snipaste_2020-08-03_15-30-29.png](http://ww1.sinaimg.cn/large/005EVOgYly1ghdnpeya8dj31e10qt0zv.jpg)
# 指令碼一覽
```python
#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: base_requests.py
@ide: PyCharm
@time: 2020/7/31
"""
from loguru import logger
import requests
class BaseRequest(object):
def __init__(self):
pass
# 請求
def base_requests(self, method, url, data=None, file_var=None, file_path=None, header=None):
"""
:param method: 請求方法
:param url: 介面path
:param data: 資料,請傳入dict樣式的字串
:param file_path: 上傳的檔案路徑
:param file_var: 介面中接收檔案物件的引數名
:param header: 請求頭
:return: 完整的響應物件
"""
session = requests.Session()
if (file_var in [None, '']) and (file_path in [None, '']):
files = None
else:
# 檔案不為空的操作
files = {file_var: open(file_path, 'rb')}
# get 請求引數傳遞形式 params
if method == 'get':
res = session.request(method=method, url=url, params=data, headers=header)
else:
res = session.request(method=method, url=url, data=data, files=files, headers=header)
logger.info(f'請求方法:{method},請求路徑:{url}, 請求引數:{data}, 請求檔案:{files}, 請求頭:{header})')
return res.json()
```
```python
#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: read_data.py
@ide: PyCharm
@time: 2020/7/31
"""
import json
import jsonpath
import xlrd
from xlutils.copy import copy
from loguru 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 = []
title_list = []
table = self.book.sheet_by_index(0)
for norw in range(1, table.nrows):
# 每行第4列 是否執行
if table.cell_value(norw, 3) == '否':
continue
# 每行第3列, 標題單獨拿出來
title_list.append(table.cell_value(norw, 1))
# 返回該行的所有單元格組成的資料 table.row_values(0) 0代表第1列
case_number = table.cell_value(norw, 0)
path = table.cell_value(norw, 2)
is_token = table.cell_value(norw, 4)
method = table.cell_value(norw, 5)
file_var = table.cell_value(norw, 6)
file_path = table.cell_value(norw, 7)
dependent = table.cell_value(norw, 8)
data = table.cell_value(norw, 9)
expect = table.cell_value(norw, 10)
actual = table.cell_value(norw, 11)
value = [case_number, path, is_token, method, file_var, file_path, dependent, data, expect, actual]
logger.info(value)
# 配合將每一行轉換成元組儲存,迎合 pytest的引數化操作,如不需要可以註釋掉 value = tuple(value)
value = tuple(value)
data_list.append(value)
return data_list, title_list
def write_result(self, case_number, result):
"""
:param case_number: 用例編號:case_001
:param result: 需要寫入的響應值
:return:
"""
row = int(case_number.split('_')[1])
logger.info('開始回寫實際響應結果到用例資料中.')
result = json.dumps(result, ensure_ascii=False)
new_excel = copy(self.book)
ws = new_excel.get_sheet(0)
# 11 是 實際響應結果欄在excel中的列數-1
ws.write(row, 11, result)
new_excel.save(self.excel_file)
logger.info(f'寫入完畢:-寫入檔案: {self.excel_file}, 行號: {row + 1}, 列號: 11, 寫入值: {result}')
# 讀實際的響應
def read_actual(self, depend):
"""
:param nrow: 列號
:param depend: 依賴資料字典格式,前面用例編號,後面需要提取對應欄位的jsonpath表示式
{"case_001":["$.data.id",],}
:return:
"""
depend = json.loads(depend)
# 用來存依賴資料的字典
depend_dict = {}
for k, v in depend.items():
# 得到行號
norw = int(k.split('_')[1])
table = self.book.sheet_by_index(0)
# 得到對應行的響應, # 11 是 實際響應結果欄在excel中的列數-1
actual = json.loads(table.cell_value(norw, 11))
try:
for i in v:
logger.info(f'i {i}, v {v}, actual {actual} \n {type(actual)}')
depend_dict[i.split('.')[-1]] = jsonpath.jsonpath(actual, i)[0]
except TypeError as e:
logger.error(f'實際響應結果中無法正常使用該表示式提取到任何內容,發現異常{e}')
return depend_dict
```
```python
#!/usr/bin/env/python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: test_api.py
@ide: PyCharm
@time: 2020/7/31
"""
import json
import shutil
import jsonpath
from loguru import logger
import pytest
import allure
from api.base_requests import BaseRequest
from tools.read_config import ReadConfig
from tools.read_data import ReadData
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()
data_list, title_ids = ReadData(case_data_path).get_data()
br = BaseRequest()
token_header = {}
no_token_header = {}
class TestApiAuto(object):
def start_run_test(self):
import os
if os.path.exists('../report') and os.path.exists('../log'):
shutil.rmtree(path='../report')
shutil.rmtree(path='../log')
logger.add(log_path)
pytest.main(args=[f'--alluredir={report_data}'])
# # 啟動一個web服務的報告
# os.system('allure serve ./report/data')
os.system(f'allure generate {report_data} -o {report_generate} --clean')
logger.debug('報告已生成')
def treating_data(self, is_token, dependent, data):
if is_token == '':
header = no_token_header
else:
header = token_header
logger.info(f'處理依賴時data的資料:{data}')
if dependent != '':
dependent_data = ReadData(case_data_path).read_actual(dependent)
logger.debug(f'依賴資料解析獲得的字典{dependent_data}')
if data != '':
# 合併組成一個新的data
dependent_data.update(json.loads(data))
data = dependent_data
logger.debug(f'data有資料,依賴有資料時 {data}')
else:
# 賦值給data
data = dependent_data
logger.debug(f'data無資料,依賴有資料時 {data}')
else:
if data == '':
data = None
logger.debug(f'data無資料,依賴無資料時 {data}')
else:
data = json.loads(data)
logger.debug(f'data有資料,依賴無資料 {data}')
return data, header
@pytest.mark.parametrize('case_number,path,is_token,method,file_var,'
'file_path,dependent,data,expect,actual', data_list, ids=title_ids)
def test_main(self, case_number, path, is_token, method, file_var, file_path,
dependent, data, expect, actual):
with allure.step("處理相關資料依賴,header"):
data, header = self.treating_data(is_token, dependent, data)
with allure.step("傳送請求,取得響應結果的json串"):
res = br.base_requests(method=method, url=base_url + path, file_var=file_var, file_path=file_path,
data=data, header=header)
with allure.step("將響應結果的內容寫入用例中的實際結果欄"):
ReadData(case_data_path).write_result(case_number, res)
# 寫token的介面必須是要正確無誤能返回token的
if is_token == '寫':
with allure.step("從登入後的響應中提取token到header中"):
token_header['Authorization'] = jsonpath.jsonpath(res, token_reg)[0]
logger.info(f'token_header: {token_header}, \n no_token_header: {no_token_header}')
with allure.step("根據配置檔案的提取響應規則提取實際資料"):
really = jsonpath.jsonpath(res, res_reg)[0]
with allure.step("處理讀取出來的預期結果響應"):
expect = eval(expect)
with allure.step("預期結果與實際響應進行斷言操作"):
assert really == expect
logger.info(f'完整的json響應: {res}\n 需要校驗的資料字典: {really}\n 預期校驗的資料字典: {expect}\n 測試結果: {really == expect}')
if __name__ == '__main__':
from tools.zip_file import zipDir
from tools.send_email import send_email
t1 = TestApiAuto()
t1.start_run_test()
zipDir(report_generate, report_zip)
send_email(email_setting)
```
# 執行結果
![Snipaste_2020-08-03_15-54-45.png](http://ww1.sinaimg.cn/large/005EVOgYly1ghdoei7jhdj313a0r57eb.jpg)
# 致謝
jsonpath語法學習:https://blog.csdn.net/liuchunming033/article/details/106272542
zip檔案壓縮:https://www.cnblogs.com/yhleng/p/9407946.html
這算是學習介面自動化的第一個成果,但是要應用生產環境,拿過去還需要改很多東西,歡迎交流。
# 原始碼地址
原始碼地址: https://gitee.com/zy7y/apiAutoTest.git