基于pytest和allure構(gòu)建自動化測試框架與項目
代碼:https://gitee.com/kunmzhao/auto_test_-project.git
框架目錄結(jié)構(gòu)
我們要構(gòu)建一個自動化測試框架,就要以項目的概念來對項目中的所有代碼文件進行劃分目錄和文件結(jié)構(gòu),需要設(shè)計一個合理的目錄結(jié)構(gòu),以便與測試開發(fā)團隊的其他人員的開發(fā)和測試,也便于項目的維護
設(shè)計的項目目錄如下
根目錄
├── api # 封裝測試項目的api接口[用于mock測試]
│ └── __init__.py
├── config.py # 項目代碼配置文件
├── data # 測試數(shù)據(jù)/測試用例存放目錄
├── libs # 第三方lib庫
├── main.py # 項目入口
├── pytest.ini # pytest模塊的配置文件
├── reports # HTML測試報告生成目錄
├── results # 測試報告生成目錄
├── tests # 測試用例腳本存放目錄
├── utils # 自定義工具類
├── requirments.txt # 項目依賴模塊
配置文件 config.py
import pathlib # 路徑操作模塊,替代os.path模塊,可以通過對象的方式操作路徑 # 項目目錄的主目錄文件路徑[字符串] BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix() # 項目目錄的主目錄路徑[路徑對象] BASE_DIR = pathlib.Path(BASE_DIR_STR) # 項目名字 PROJECT_NAME = "接口自動化測試框架" # 自動化測試項目的運行IP和端口 HOST = "127.0.0.1" PORT = 8088 if __name__ == '__main__': print(BASE_DIR_STR, type(BASE_DIR_STR)) print(BASE_DIR, type(BASE_DIR))
入口文件main.py
import pytest import os import sys import shutil import config if __name__ == '__main__': try: # 刪除之前存在的報告文件夾 shutil.rmtree("./reports") shutil.rmtree("./results") except Exception as e: # TODO:需要添加日志 print(e) # 啟動 pytest 測試框架,啟動參數(shù)配置在 pytest.ini 中 pytest.main() # 生成測試報告 os.system(f"{config.BASE_DIR.joinpath('libs/allure/bin/allure')} generate ./results -o ./reports") # 打開測試報告 os.system(f"{config.BASE_DIR.joinpath('libs/allure/bin/allure')} serve ./results -h {config.HOST} -p {config.PORT}")
pytest.ini配置文件
[pytest] # 指定運行參數(shù) addopts = -s -v -p no:warnings --alluredir=./results # 搜索測試文件的目錄路徑 testpaths = ./ # 搜索測試文件名格式 python_files = test_*.py # 搜索測試類格式 python_classes = Test* # 搜索測試方法名格式 python_functions = test_*
下面編寫一個測試用例,來確保框架可以正常運行
test_login.py
import allure import config @allure.epic(config.PROJECT_NAME) @allure.feature("用戶模塊") @allure.story("用戶登錄") class TestUser(object): def test_username_by_empty(self): allure.dynamic.title("用戶名為空,登錄失敗") allure.dynamic.description("測試用戶名為空的描述") allure.attach("文件內(nèi)容", "log") assert 1 == 2
運行main.py

新增日志功能
日志模塊是項目中必不可缺的模塊,便于我們查看項目運行狀況和排查錯誤
config.py新增日志配置
import pathlib # 路徑操作模塊,替代os.path模塊,可以通過對象的方式操作路徑 # 項目目錄的主目錄文件路徑[字符串] BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix() # 項目目錄的主目錄路徑[路徑對象] BASE_DIR = pathlib.Path(BASE_DIR_STR) # 項目名字 PROJECT_NAME = "接口自動化測試框架" # 自動化測試報告的運行IP和端口 HOST = "127.0.0.1" PORT = 8088 # 日志模塊配置 LOGGING = { "name": "Zeekr", # 日志處理器名稱 "filename": (BASE_DIR / "logs/zeeker.log").as_posix(), # 日志文件儲存路徑 "charset": "utf-8", "backup_count": 31, # 日志文件的備份數(shù)量 "when": "d" # 日志文件創(chuàng)建間隔時間為每天創(chuàng)建一個 } if __name__ == '__main__': print(BASE_DIR_STR, type(BASE_DIR_STR)) print(BASE_DIR, type(BASE_DIR))
logger.py模塊
import logging import config from logging import handlers class LogHandler(object): """日志處理工具類""" def __init__(self, name=None, filename=None): """ :param name: 日志處理器的名字 :param filename: 日志文件名字 """ # 如果沒有指定name和filename,則使用配置文件配置 self.name = name or config.LOGGING.get("name", "pytest") self.filename = filename or config.LOGGING.get("filename", "pytest.log") self.charset = config.LOGGING.get("charset", "utf-8") self.backup_count = config.LOGGING.get("backup_count", 31) self.when = config.LOGGING.get("when", "d") self.logger = None def get_logger(self): """ 創(chuàng)建 logger :return: logger 對象 """ # 避免重復(fù)創(chuàng)建logger TODO:使用到單例模式是否更好? if self.logger: return self.logger logger = logging.getLogger(self.name) # 設(shè)置日志初始化等級 logger.setLevel(logging.DEBUG) # 創(chuàng)建handler fh = handlers.TimedRotatingFileHandler( filename=self.filename, when=self.when, backupCount=self.backup_count, encoding=self.charset ) sh = logging.StreamHandler() # 為每種handler設(shè)置日志等級 # fh.setLevel(logging.INFO) # 設(shè)置輸出日志格式 simple_formater = logging.Formatter(fmt="【{levelname}】 {name} {module}: {lineno} {message}", style="{") verbose_formater = logging.Formatter( fmt="【{levelname}】 {asctime} {name} {pathname}: {lineno} {message}", datefmt="%Y-%m-%d %H:%M:%S", style="{" ) # 為handler指定輸出格式 fh.setFormatter(verbose_formater) sh.setFormatter(simple_formater) # 為logger添加日志處理器 logger.addHandler(fh) logger.addHandler(sh) self.logger = logger return logger if __name__ == '__main__': logger = LogHandler().get_logger() logger.debug("測試 debug") logger.info("測試 info") logger.warning("測試 warning") logger.error("測試 error")
在測試用例中簡單使用日志
import allure import config from utils.logger import LogHandler logger = LogHandler().get_logger() @allure.epic(config.PROJECT_NAME) @allure.feature("用戶模塊") @allure.story("用戶登錄") class TestUser(object): def test_username_by_empty(self): allure.dynamic.title("用戶名為空,登錄失敗") allure.dynamic.description("測試用戶名為空的描述") allure.attach("文件內(nèi)容", "log") logger.debug("測試 debug") logger.info("測試 info") logger.warning("測試 warning") logger.error("測試 error")
運行main.py文件

【DEBUG】 2023-09-26 10:16:33 Zeekr /home/zk4956z3/PycharmProjects/Stark/utils/logger.py: 75 測試 debug 【INFO】 2023-09-26 10:16:33 Zeekr /home/zk4956z3/PycharmProjects/Stark/utils/logger.py: 76 測試 info 【W(wǎng)ARNING】 2023-09-26 10:16:33 Zeekr /home/zk4956z3/PycharmProjects/Stark/utils/logger.py: 77 測試 warning 【ERROR】 2023-09-26 10:16:33 Zeekr /home/zk4956z3/PycharmProjects/Stark/utils/logger.py: 78 測試 error 【DEBUG】 2023-09-26 10:18:34 Zeekr /home/zk4956z3/PycharmProjects/Stark/tests/users/test_login.py: 15 測試 debug 【INFO】 2023-09-26 10:18:34 Zeekr /home/zk4956z3/PycharmProjects/Stark/tests/users/test_login.py: 16 測試 info 【W(wǎng)ARNING】 2023-09-26 10:18:34 Zeekr /home/zk4956z3/PycharmProjects/Stark/tests/users/test_login.py: 17 測試 warning 【ERROR】 2023-09-26 10:18:34 Zeekr /home/zk4956z3/PycharmProjects/Stark/tests/users/test_login.py: 18 測試 error 【DEBUG】 2023-09-27 09:02:13 Zeekr /home/zk4956z3/PycharmProjects/Stark/tests/users/test_login.py: 16 測試 debug 【INFO】 2023-09-27 09:02:13 Zeekr /home/zk4956z3/PycharmProjects/Stark/tests/users/test_login.py: 17 測試 info 【W(wǎng)ARNING】 2023-09-27 09:02:13 Zeekr /home/zk4956z3/PycharmProjects/Stark/tests/users/test_login.py: 18 測試 warning 【ERROR】 2023-09-27 09:02:13 Zeekr /home/zk4956z3/PycharmProjects/Stark/tests/users/test_login.py: 19 測試 error
封裝請求工具
接口的測試一般離不開http請求,在python中常用的http請求模塊有urllib, requests,httpx等,不過小編最常用的還是requests
安裝
pip install requests
簡單使用
import requests
####### GET請求 ########
# 發(fā)送簡單的get請求 response = requests.get("https://baidu.com")
# 發(fā)送有參數(shù)的get請求
params = {
"name": "kunmzhao",
"age":18
} response = requests.get("https://httpbin.org", params=params) # 獲取原生內(nèi)容 print(response.content) # 獲取文本內(nèi)容 print(response.text)
# 接收json格式內(nèi)容
print(response.json())
# 接收二進制內(nèi)容
with open("1.png", "wb") as fd:
fd.write(response.content)
####### POST請求 ########
# 發(fā)送表單數(shù)據(jù)
import requests
forms = {"name":"kunmzhao","age":18}
response = requests.post("http://httpbin.org/post", data=forms)
print(response.text)
# 發(fā)送json數(shù)據(jù)
json_data = {"name":"kunmzhao","age":18}
response = requests.post("http://httpbin.org/post", json=json_data)
print(response.text)
# 文件上傳 支持一張或者多張數(shù)據(jù)的上傳
files = {"avata":open("1.png",'rb')}
response = requests.post("http://httpbin.org/post", files=files)
print(response.text)
##### 發(fā)送請求頭 #####
headers = {
"User-Agent":"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/117.0"
}
response = requests.get("https://www/zhihu.com/explore", headers=headers)
print(response.text)
requestor.py 對常用的http請求操作進行封裝
import requests from utils.logger import LogHandler class Request(object): """http請求工具類""" def __init__(self): # 實例化session管理器,維持會話 self.session = requests.session() self.logger = LogHandler().get_logger() def send(self, method, url, params=None, data=None, json=None, headers=None, **kwargs): """ 發(fā)送http請求 :param method: 請求方法 :param url: 請求URL :param params: 請求參數(shù) :param data: 請求數(shù)據(jù) :param json: jason傳參,請求數(shù)據(jù) :param headers: : 請求頭 :param kwargs: 其它參數(shù) :return: headers """ try: self.logger.info(f"請求方法: {method}") self.logger.info(f"請求url: {url}") self.logger.info(f"請求params: {params}") self.logger.info(f"請求data: {data}") self.logger.info(f"請求json: {json}") self.logger.info(f"請求headers: {headers}") self.logger.info(f"請求額外參數(shù): {kwargs}") response = self.session.request(method=method, url=url, params=params, data=data, json=json, headers=headers, **kwargs) self.logger.info(f"回復(fù)狀態(tài)碼: {response.status_code}") self.logger.info(f"回復(fù)響應(yīng)頭: {response.headers}") self.logger.info(f"回復(fù)響應(yīng)體[二進制]: {response.content}") self.logger.info(f"回復(fù)響應(yīng)體[純文本]: {response.text}") self.logger.info(f"回復(fù)響應(yīng)體[json]: {response.json()}") return response except Exception as e: self.logger.error(f"請求錯誤,錯誤信息:{e}") def __call__(self, method, url, params=None, data=None, json=None, headers=None, **kwargs): """ 將對象當(dāng)做一個函數(shù)來使用,Request(method, url, params=None, data=None, json=None, headers=None, **kwargs) """ return self.send(method=method, url=url, params=params, data=data, json=json, headers=headers, **kwargs) if __name__ == '__main__': request = Request() res = request("GET", "http://httpbin.org/get") print(res) res = request("post", "http://httpbin.org/post", json={"username": "kunmzhao"}) print(res)
基于Flask四線Mockserver
在實際的項目開發(fā)中,經(jīng)常出現(xiàn)服務(wù)端和客戶端分離的情況,我們在做測試開發(fā)的時候,有可能服務(wù)的功能還沒有實現(xiàn),我們可以自己先模擬出服務(wù)端返回結(jié)果,以實現(xiàn)聯(lián)調(diào),等服務(wù)器上線后,切換server即可
Flask是一個輕量級的python web框架,非常適合在測試中構(gòu)建模擬api服務(wù)器
安裝模塊
pip install flask
pip install pymysql
pip install flask_sqlalchemy
api/__init__.py
import config from flask import Flask from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() app = Flask(__name__) def init_app(): # 加載配置 app.config.from_object(config) # 加載數(shù)據(jù)庫配置 db.init_app(app) # db創(chuàng)建數(shù)據(jù)庫 with app.app_context(): db.create_all() return app
config.py
import pathlib # 路徑操作模塊,替代os.path模塊,可以通過對象的方式操作路徑 # 項目目錄的主目錄文件路徑[字符串] BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix() # 項目目錄的主目錄路徑[路徑對象] BASE_DIR = pathlib.Path(BASE_DIR_STR) # 項目名字 PROJECT_NAME = "接口自動化測試框架" # 自動化測試報告的運行IP和端口 HOST = "127.0.0.1" PORT = 8089 # 日志模塊配置 LOGGING = { "name": "Zeekr", # 日志處理器名稱 "filename": (BASE_DIR / "logs/zeeker.log").as_posix(), # 日志文件儲存路徑 "charset": "utf-8", "backup_count": 31, # 日志文件的備份數(shù)量 "when": "d" # 日志文件創(chuàng)建間隔時間為每天創(chuàng)建一個 } """mock server 的服務(wù)端配置""" # 數(shù)據(jù)庫連接 SQLALCHEMY_DATABASE_URI: str = "mysql+pymysql://root:123@127.0.0.1:3306/pytest?charset=utf8mb4" # 查詢時會顯示原始SQL語句 SQLALCHEMY_ECHO: bool = True # 調(diào)試模式 DEBUG = True # 監(jiān)聽端口 API_PORT = 8000 # 監(jiān)聽地址 API_HOST = "0.0.0.0" if __name__ == '__main__': print(BASE_DIR_STR, type(BASE_DIR_STR)) print(BASE_DIR, type(BASE_DIR))
api/models.py
from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from . import db class BaseModel(db.Model): """公共模型""" __abstract__ = True # 抽象模型 id = db.Column(db.Integer, primary_key=True, comment="主鍵ID") name = db.Column(db.String(255), default="", comment="名稱/標題") is_deleted = db.Column(db.Boolean, default=False, comment="邏輯刪除") orders = db.Column(db.Integer, default=0, comment="排序") status = db.Column(db.Boolean, default=True, comment="狀態(tài)(是否顯示,是否激活)") created_time = db.Column(db.DateTime, default=datetime.now, comment="創(chuàng)建時間") updated_time = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment="更新時間") def __repr__(self): return f"<{self.__class__.__name__}: {self.name}>" class User(BaseModel): """用戶基本信息表""" __tablename__ = "py_user" name = db.Column(db.String(255), index=True, comment="用戶賬戶") nickname = db.Column(db.String(255), comment="用戶昵稱") _password = db.Column(db.String(255), comment="登錄密碼") intro = db.Column(db.String(500), default="", comment="個性簽名") avatar = db.Column(db.String(255), default="", comment="頭像url地址") sex = db.Column(db.SmallInteger, default=0, comment="性別") # 0表示未設(shè)置,保密, 1表示男,2表示女 email = db.Column(db.String(32), index=True, default="", nullable=False, comment="郵箱地址") mobile = db.Column(db.String(32), index=True, nullable=False, comment="手機號碼") # 存取器 @property def password(self): # user.password return self._password @password.setter def password(self, rawpwd): # user.password = '123456' """密碼加密""" self._password = generate_password_hash(rawpwd) def check_password(self, rawpwd): """驗證密碼""" return check_password_hash(self.password, rawpwd)
api/views.py
from sqlalchemy import or_
from . import app
from .models import User,db
@app.route("/user/register", methods=["POST"])
def register():
"""
用戶信息注冊
:return:
"""
try:
data = request.json
# 創(chuàng)建用戶數(shù)據(jù)
user = User(**data)
db.session.add(user)
db.session.commit()
return {"msg": "注冊成功!", "data": {"id":user.id, "name": user.name}}, 200
except Exception as e:
return {"msg": "注冊失敗!", "data": {}}, 400
@app.route("/user/login", methods=["POST"])
def login():
"""
用戶登錄
:return:
"""
user = User.query.filter(
or_(
User.mobile == request.json.get("mobile"),
User.name == request.json.get("name"),
User.email == request.json.get("email")
)
).first() # 實例化模型
if not user:
return {"msg": "登錄失敗!用戶不存在!", "data": {}}, 400
if not user.check_password(request.json.get("password")):
return {"msg": "登錄失敗!密碼錯誤!", "data": {}}, 400
return {"msg": "登錄成功", "data":{"id": user.id, "name": user.name}}, 200
api/run.py
import config from api import init_app # 注意,務(wù)必把模型models的內(nèi)容以及 views 中的服務(wù)端接口引入當(dāng)前文件,否則flask不識別。 from api import models from api import views app = init_app() if __name__ == '__main__': app.run(host=config.API_HOST, port=config.API_PORT)
test_login.py
import allure import config from utils.logger import LogHandler from utils.requestor import Request logger = LogHandler().get_logger() SERVER_URl = f"http://{config.API_HOST}:{config.API_PORT}" @allure.epic(config.PROJECT_NAME) @allure.feature("用戶模塊") @allure.story("用戶登錄") class TestUser(object): def test_username_by_empty(self): allure.dynamic.title("用戶名為空,登錄失敗") allure.dynamic.description("測試用戶名為空的描述") allure.attach("文件內(nèi)容", "log") request = Request() response = request("POST", f"{SERVER_URl}/user/login", json={ "name": "", "password": "123" }) print(response) def test_password_by_empty(self): allure.dynamic.title("密碼為空,登錄失敗") allure.dynamic.description("測試密碼為空的描述") allure.attach("文件內(nèi)容", "log") request = Request() response = request("POST", f"{SERVER_URl}/user/login", json={ "name": "kunmzhao", "password": "" }) print(response)
此時運行main.py文件和run.py文件,即可測該框架是否成功
基于數(shù)據(jù)驅(qū)動生成用例代碼
在實際測試開發(fā)中,我們一般使用參數(shù)化來自動生成測試用例,參數(shù)化用例一般采用json,yaml或者excle文件儲存,如果用例非常多,也可以改用數(shù)據(jù)庫
下面介紹通過excel來承載測試用例,python中操作excel文件的模塊有xlrd+xlwt,pyexcle+openpyxl
安裝模塊
pip install xlrd
pip install xlwt
封裝excel工具類
excel.py
""" 不識別xlsx的文件后綴名,文件格式必須設(shè)置為xls """ import xlrd import json class Excel(object): """Excel文件寫工具類, TODO:關(guān)于excel寫操作后續(xù)補充""" def __init__(self, filename): self.workbook = xlrd.open_workbook(filename, formatting_info=True) def get_sheet_names(self): """ 獲取當(dāng)前excle中所有sheet的名字 """ return self.workbook.sheet_names() def __get_sheet(self, sheet_index_or_name): """ 根據(jù)sheet的索引或者名字獲取對應(yīng)sheet對象 """ if isinstance(sheet_index_or_name, int): if len(self.get_sheet_names()) > sheet_index_or_name: return self.workbook.sheet_by_index(sheet_index_or_name) raise Exception( "無效的的sheet下標數(shù)值, excel文檔最大值為{}, 請求為數(shù)值為{}".format( len(self.get_sheet_names()), sheet_index_or_name), ) elif isinstance(sheet_index_or_name, str): if sheet_index_or_name in self.get_sheet_names(): return self.workbook.sheet_by_name(sheet_index_or_name) raise Exception("無效的sheet名字,名字為{}的sheet不存在".format(sheet_index_or_name)) raise Exception("sheet_index_or_name只能為int或者str,但是入?yún)閧}".format(type(sheet_index_or_name))) def get_sheet_rows_num(self, sheet_index_or_name): """ 獲取指定sheet的的數(shù)據(jù)總行數(shù) """ return self.__get_sheet(sheet_index_or_name).nrows def get_sheet_cols_num(self, sheet_index_or_name): """ 獲取指定sheet的數(shù)據(jù)總列數(shù) """ return self.__get_sheet(sheet_index_or_name).ncols def get_cell_value(self, sheet_index_or_name, row_index, col_index): """ 獲取指定sheet中的指定單元格數(shù)據(jù) """ sheet = self.__get_sheet(sheet_index_or_name) try: return sheet.cell_value(row_index, col_index) except Exception as e: raise Exception(str(e)) def get_sheet_data(self, sheet_index_or_name, fields, first_line_is_header=True): """ 獲取工作表的所有數(shù)據(jù) """ rows = self.get_sheet_rows_num(sheet_index_or_name) cols = self.get_sheet_cols_num(sheet_index_or_name) data = [] for row in range(int(first_line_is_header), rows): row_data = {} for col in range(cols): cell_data = self.get_cell_value(sheet_index_or_name, row, col) if type(cell_data) is str and ("{" in cell_data and "}" in cell_data) or ( "[" in cell_data and "]" in cell_data): cell_data = json.loads(cell_data) print(col, fields[col]) row_data[fields[col]] = cell_data data.append(row_data) return data if __name__ == '__main__': xls = Excel("../data/外來人員.xls") fields = ["姓名", "身份證號", "手機號", "人員在膠狀態(tài)", "目前詳細住址", "隨訪異常", "隨訪異常備注", "是否已做核酸", "核酸結(jié)果", "核酸采樣日期", "核酸采樣地點", "人員類別", "重點地區(qū)", "到達重點地區(qū)時間", "離開重點地區(qū)時間", "返回膠州日期", "請輸入行程詳情概述"] print(xls.get_sheet_names()) print(xls.get_sheet_cols_num("Sheet0")) print(xls.get_sheet_rows_num(0)) print(xls.get_cell_value(0, 1, 1)) print(xls.get_sheet_data(0, fields))
config.py
import pathlib # 路徑操作模塊,替代os.path模塊,可以通過對象的方式操作路徑 # 項目目錄的主目錄文件路徑[字符串] BASE_DIR_STR = pathlib.Path(__file__).parent.resolve().as_posix() # 項目目錄的主目錄路徑[路徑對象] BASE_DIR = pathlib.Path(BASE_DIR_STR) # 項目名字 PROJECT_NAME = "接口自動化測試框架" # 自動化測試報告的運行IP和端口 HOST = "127.0.0.1" PORT = 8089 # 日志模塊配置 LOGGING = { "name": "Zeekr", # 日志處理器名稱 "filename": (BASE_DIR / "logs/zeeker.log").as_posix(), # 日志文件儲存路徑 "charset": "utf-8", "backup_count": 31, # 日志文件的備份數(shù)量 "when": "d" # 日志文件創(chuàng)建間隔時間為每天創(chuàng)建一個 } # excel測試用例字段格式 FIELD_LIST = [ "case_id", # 用例編號 "module_name", # 模塊名稱 "case_name", # 用例名稱 "method", # 請求方式 "url", # 接口地址 "headers", # 請求頭 "params_desc", # 參數(shù)說明 "params", # 請求參數(shù) "assert_result", # 預(yù)期結(jié)果 "real_result", # 實際結(jié)果 "remark", # 備注 ] """mock server 的服務(wù)端配置""" # 數(shù)據(jù)庫連接 SQLALCHEMY_DATABASE_URI: str = "mysql+pymysql://root:ZKMzkm36337@127.0.0.1:3306/pytest?charset=utf8mb4" # 查詢時會顯示原始SQL語句 SQLALCHEMY_ECHO: bool = True # 調(diào)試模式 DEBUG = True # 監(jiān)聽端口 API_PORT = 8000 # 監(jiān)聽地址 API_HOST = "0.0.0.0" if __name__ == '__main__': print(BASE_DIR_STR, type(BASE_DIR_STR)) print(BASE_DIR, type(BASE_DIR))

utils/assertor.py
from utils.logger import LogHandler logger = LogHandler().get_logger() def assertor(assert_list, response): """斷言函數(shù)""" if type(assert_list) is not list: assert_list = [assert_list] for expr in assert_list: logger.info(f"開始斷言:assert {expr}") if expr: # exec 內(nèi)置解釋器,可以把符合python語法的字符串當(dāng)成代碼來運行 exec(f"assert {expr}", { "code": response.status_code, "json": response.json(), "text": response.text, "content": response.content, "headers": response.headers, }) logger.info(f"斷言通過:assert {expr}") if __name__ == '__main__': # Response就是模擬requests HTTP請求工具的返回結(jié)果對象 class Response(object): status_code = 400 text = "對不起,登陸失敗!" content = "對不起,登陸失敗!" headers = [] @classmethod def json(cls): return {"id": 1}, assert_list = [ "code == 400", "'失敗'in text", ] assertor(assert_list, Response())
test_login.py
import allure import pytest import config from utils.logger import LogHandler from utils.requestor import Request from utils.excle import Excel from utils.assertor import assertor logger = LogHandler().get_logger() SERVER_URl = f"http://{config.API_HOST}:{config.API_PORT}" @allure.epic(config.PROJECT_NAME) @allure.feature("用戶模塊") @allure.story("用戶登錄") class TestUser(object): @pytest.mark.parametrize("kwargs", Excel(config.BASE_DIR / "data/case_user.xls").get_sheet_data(0, config.FIELD_LIST)) def test_login(self, kwargs): request = Request() allure.dynamic.title(kwargs.get("case_name")) request.logger.info(f"開始請求測試接口:{kwargs.get('case_name')}") if kwargs.get("method").lower() in ["get", "delete"]: """發(fā)送get或delete""" response = request(kwargs.get("method"), f'{kwargs.get("url")}', params=kwargs.get("params")) else: """發(fā)送post,put,patch""" response = request(kwargs.get("method"), f'{kwargs.get("url")}', json=kwargs.get("params")) assertor(kwargs.get("assert_result"), response)
浙公網(wǎng)安備 33010602011771號