pytest+request+allure 介面自動化框架搭建分享
去年 11 月被裁,到現在還沒上岸,gap 半年了。上岸無望,專業技能不能落下,花了兩三天時間,把之前工作中搭建使用的介面自動化框架,重寫了一套。
樓主程式碼菜雞,程式碼可能比較 low - -,
希望透過本次分享,給社群裡想寫介面自動化的同學一些借鑑,也希望社群裡的大神多給一些最佳化建議,大家互幫互助,共同進步~
框架基於 python 語言,框架使用 pytest,報告使用 allure
支援多環境執行,透過命令列傳參區分
支援多程序跑測,用例需獨立無依賴,conftest.py 中包含多程序下只執行一次的 fileLock fixture
支援資料庫連線單例,一個庫在一個程序下只會建立一次連線
支援 mysql、redis 操作
支援 get、post、put、delete 請求方法,請求是透過用例的請求頭 Content-Type 來區分,是使用 params、data 還是 json 傳參
支援引數化資料驅動,用引數化引數字典,去更新通用引數字典,更新後發起請求
以下使用 windows 環境
conda 配置和新建工程:
安裝 conda
https://www.anaconda.com/download/success
新建工程,在 pycharm 中新建 conda 虛擬環境
安裝 allure 報告
https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/2.29.0/
解壓並配置環境變數
D:\allure\allure-2.29.0
cmd 命令列,驗證 allure 安裝
設定 pycharm 檔案編碼 UTF-8
依賴安裝
pycharm 命令列執行
啟用虛擬環境
conda activate pytest_api_auto
安裝 python3.8
conda install python=3.8
安裝依賴包
pip install requests
pip install jsonpath
pip install pytest
pip install allure-pytest
pip install pytest-sugar
pip install pytest-xdist
pip install pytest-assume
pip install pymysql
pip install redis
pip install faker
pip install filelock
目錄劃分
以下是原始碼部分
封裝 log 日誌工具類
# common/log_util.py
import logging
import os
# create logger
log = logging.getLogger("pytest_api_auto")
log.setLevel(logging.INFO)
# create file handler
# mode 預設為a追加模式,如果修改為w為覆蓋模式,多程序執行會出現日誌缺失和錯亂
# 獲取專案根目錄拼接,日誌會存在工程根目錄pytest.log 每次執行追加寫入
fh = logging.FileHandler(os.path.join(os.path.dirname(os.path.dirname(__file__)), "pytest.log"),
mode='a', encoding='UTF-8')
fh.setLevel(logging.INFO)
# create stream handler
sh = logging.StreamHandler(stream=None)
# create formatter
fmt = "%(asctime)s - %(filename)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s"
formatter = logging.Formatter(fmt)
# add handler and formatter to logger
fh.setFormatter(formatter)
sh.setFormatter(formatter)
log.addHandler(fh)
log.addHandler(sh)
封裝環境配置
config/env.py
from common.log_util import log
class ENV:
# 環境資訊:test 測試 prod 準生產 # 從pytest命令列獲取
info = None
# 測試環境服務域名配置
class UrlTestConfig:
api_backend = "http://api_backend.cn:8899"
# 準生產環境服務域名配置
class UrlProdConfig:
api_backend = "http://api_backend.cn:8899"
def get_url(server_name):
if ENV.info == "test":
url = getattr(UrlTestConfig, server_name)
log.info(f"測試環境獲取服務域名 - {server_name} : {url}")
return url
elif ENV.info == "prod":
url = getattr(UrlProdConfig, server_name)
log.info(f"準生產環境獲取服務域名 - {server_name} : {url}")
return url
else:
raise Exception("--env 環境資訊有誤")
封裝 mysql 操作工具類
# common/mysql_util.py
# 裝飾器,同一個mysql資料庫只建立一次連線
import pymysql
from time import sleep
from common.log_util import log
# 裝飾器,同一個mysql資料庫只建立一次連線
def decorate_single(cls):
connect_list = {}
def wrapper(*args, **kwargs):
nonlocal connect_list
db_name = args[0]["db"]
if db_name not in connect_list:
connect_list[db_name] = cls(*args, **kwargs)
log.info(f"建立mysql連線並返回 - {db_name}")
else:
log.info(f"mysql連線已建立,直接返回 - {db_name}")
return connect_list[db_name]
return wrapper
@decorate_single
class MySql:
def __init__(self, db_config: dict):
"""
:params: db_config 資料庫配置 型別為字典
"""
# 資料庫配置
# autocommit: True 選項很關鍵,如果不設定,新增資料無法查出
# mysql預設資料引擎是innodb 預設資料隔離級別重複讀,如果事務不提交,那麼每次查詢,查詢都是同一塊資料快照
self.conn = None
while True:
try:
self.conn = pymysql.connect(**db_config)
break
# 資料庫連線,偶爾會連線不上
# 報錯 pymysql.err.OperationalError: (2013, 'Lost connection to MySQL server during query')
# 解決辦法,就是重新連線
except pymysql.err.OperationalError:
log.warning("連線失敗,可能環境不穩定,重新連線!")
sleep(1)
except Exception as e:
log.warning("獲取mysql連線失敗!請檢查資料庫配置或網路連線")
raise e
def fetchone(self, sql_str: str):
"""
:params: sql_str 資料庫sql
:return: 返回查詢結果的一條記錄,型別是字典; 若未查詢到,則返回None
"""
try:
with self.conn.cursor(pymysql.cursors.DictCursor) as cursor:
log.info(f"執行sql: {sql_str}")
cursor.execute(sql_str)
data = cursor.fetchone()
log.info(f"sql執行結果: {data}")
return data
except Exception as e:
log.warning("執行sql失敗!")
raise e
def fetchall(self, sql_str: str):
"""
:params: sql_str 資料庫sql
:return: 返回查詢結果的全部記錄,型別是列表,列表元素為字典
"""
try:
with self.conn.cursor(pymysql.cursors.DictCursor) as cursor:
log.info(f"執行sql: {sql_str}")
cursor.execute(sql_str)
data = cursor.fetchall()
log.info(f"sql執行結果: {data}")
return data
except Exception as e:
log.warning("執行sql失敗!")
raise e
def execute_dml(self, sql_str):
"""
function: 執行insert、update、delete
:param sql_str 資料庫sql
:return: 無返回
"""
try:
with self.conn.cursor(pymysql.cursors.DictCursor) as cursor:
log.info(f"執行sql: {sql_str}")
data = cursor.execute(sql_str)
# 提交操作,我們配置連線是自動提交,所以下面提交步驟也可省略
self.conn.commit()
log.info(f"sql執行結果: {data}")
except Exception as e:
log.warning("執行sql失敗!")
raise e
def close(self):
"""
function:關閉資料庫連線
params: conn 資料庫連線
"""
self.conn.close()
封裝 mysql 配置和連線獲取
# config/mysql.py
from common.mysql_util import MySql
from config.env import ENV
from common.log_util import log
# 資料庫連線配置
class MysqlTestConfig:
"""Mysql測試環境配置"""
api_auto = {'host': 'localhost', 'port': 3306,
'db': 'api_auto', 'user': 'root',
'password': 'root', 'autocommit': True
}
class MysqlProdConfig:
"""Mysql準生產環境配置"""
api_auto = {'host': 'localhost', 'port': 3306,
'db': 'api_auto', 'user': 'root',
'password': 'root', 'autocommit': True
}
def get_mysql_conn(db_name):
if ENV.info == "test":
log.info("測試環境建立mysql連線 - " + db_name)
return MySql(getattr(MysqlTestConfig, db_name))
elif ENV.info == "prod":
log.info("準生產環境建立mysql連線 - " + db_name)
return MySql(getattr(MysqlProdConfig, db_name))
else:
raise Exception("--env 環境資訊有誤")
封裝 redis 工具類
# common/redis_util.py
import redis
from common.log_util import log
# 裝飾器,同一個redis只建立一次連線
def decorate_single(cls):
connect_list = {}
def wrapper(*args, **kwargs):
nonlocal connect_list
host = args[0]["host"]
if host not in connect_list:
connect_list[host] = cls(*args, **kwargs)
log.info(f"建立redis連線並返回 - {host}")
else:
log.info(f"redis連線已建立,直接返回 - {host}")
return connect_list[host]
return wrapper
@decorate_single
class Redis:
def __init__(self, db_config):
"""
:params: db_config 資料庫配置 型別為字典
"""
self.pool = redis.ConnectionPool(**db_config)
self.rs = redis.Redis(connection_pool=self.pool)
def del_key(self, key):
"""
:param key: redis key str字元型別
:return: 刪除成功返回 True 否則 False
"""
log.info(f"redis 刪除key {key}")
if self.rs.delete(key) == 1:
log.info(f"key {key} 刪除成功")
return True
else:
log.warning(f"key: {key} 不存在!")
return False
def del_keys(self, keys_pattern):
"""
:param keys_pattern: key萬用字元 str字元型別 ex: *name*
:return:刪除成功返回 True 否則 False
"""
log.info(f"redis 刪除keys 萬用字元 {keys_pattern}")
keys = self.rs.keys(keys_pattern)
if keys:
log.info(f"redis 刪除keys {keys}")
for k in keys:
self.rs.delete(k)
log.info(f"keys {keys} 刪除成功")
return True
else:
log.warning("萬用字元未匹配到key!")
return False
def set(self, key, value, ex=8 * 60 * 60):
"""
操作str型別
:param key: redis key str字元型別
:param value: str字元型別
:param ex: 資料超時時間,預設8小時
return: 寫入成功返回 True
"""
log.info(f"redis str型別 資料寫入 key: {key} value: {value}")
return self.rs.set(key, value, ex=ex)
def get(self, key):
"""
操作str型別
:param key: redis key str字元型別
:return: 獲取到返回str字元型別 # 未獲取到返回 None
"""
data = self.rs.get(key)
log.info(f"redis str型別 資料獲取 key: {key} value: {data}")
return data
def lrange(self, key):
"""
操作list型別
:param key: redis key str字元型別
return: 獲取到返回list列表型別 # 未獲取到返回空列表 []
"""
data = self.rs.lrange(key, 0, -1)
log.info(f"redis list型別 資料獲取 key: {key} values: {data}")
return data
def smembers(self, key):
"""
操作 set 集合
:param key: redis key str字元型別
return: 獲取到返回set集合型別 # 未獲取到返回空集合 set()
"""
data = self.rs.smembers(key)
log.info(f"redis set型別 資料獲取 key: {key} values: {data}")
return data
def zrange(self, key):
"""
操作 zset 有序集合
:param key: redis key str字元型別
return: 獲取到返回list列表型別 # 未獲取到返回空列表 []
"""
data = self.rs.zrange(key, 0, -1)
log.info(f"redis zset型別 資料獲取 key: {key} values: {data}")
return data
# hash 操作 hset hget 後續可擴充套件
def close(self):
"""
function:關閉資料庫連線
params: rs Redis物件
"""
self.rs.close()
封裝 redis 配置和連線獲取
# config/redis.py
from common.redis_util import Redis
from config.env import ENV
from common.log_util import log
class RedisTestConfig:
api_backend = {'host': 'api_backend.cn', 'password': 'redis123',
'port': 6379, 'db': 0, 'decode_responses': True}
class RedisProdConfig:
api_backend = {'host': 'api_backend.cn', 'password': 'redis123',
'port': 6379, 'db': 0, 'decode_responses': True}
def get_redis_conn(name):
if ENV.info == "test":
log.info("測試環境建立redis連線 - " + name)
return Redis(getattr(RedisTestConfig, name))
elif ENV.info == "prod":
log.info("準生產環境建立redis連線 - " + name)
return Redis(getattr(RedisProdConfig, name))
else:
raise Exception("--env 環境資訊有誤")
封裝 requests 工具類
# common/requests_util.py
import requests
from common.log_util import log
def send_request(url, method, data, headers, **kwargs):
"""
:param url: 請求域名 型別 str ex: http://xxx.com/path
:param method: 請求方法 型別 str 暫時支援 get、post、put、delete
:param data: 請求資料,型別 dict、list、str
:param headers: 請求頭,型別 dict
:param kwargs: 擴充套件支援 files 上傳檔案、proxy 代理等
:return:
"""
if not url.startswith("http://") and not url.startswith("https://"):
raise Exception("請求url缺少協議名")
if method.lower() not in ("get", "post", "put", "delete"):
raise Exception(f"暫不支援請求方法 - {method} - 可後續擴充套件")
log.info("請求引數:")
log.info(f"url: {url}")
log.info(f"method: {method}")
log.info(f"data: {data}")
log.info(f"headers: {headers}")
log.info(f"kwargs: {kwargs}")
try:
if "Content-Type" in headers.keys():
# headers 包含傳參型別
if headers["Content-Type"] in ("application/x-www-form-urlencoded", "multipart/form-data"):
res = requests.request(url=url, method=method, data=data, headers=headers, timeout=30, **kwargs)
elif headers["Content-Type"] == "application/json":
res = requests.request(url=url, method=method, json=data, headers=headers, timeout=30, **kwargs)
else: # 若非上面三種型別,預設使用json傳參 text/html, text/plain等,可後續擴充套件驗證
res = requests.request(url=url, method=method, json=data, headers=headers, timeout=30, **kwargs)
else:
# 請求頭沒指定傳參型別Content-Type,則使用params傳參,即在url中傳參,如get請求
res = requests.request(url=url, method=method, params=data, headers=headers, timeout=30, **kwargs)
except Exception as e:
log.warning("請求發生異常!!!")
raise e
if res.status_code == 200:
log.info("請求成功")
log.info("響應引數:")
log.info(f"{res.text}")
else:
log.warning(f"請求失敗!!! 返回碼不為200, 狀態碼為: {res.status_code}")
log.warning(f"響應引數:")
log.warning(f"text: {res.text}")
log.warning(f"raw: {res.raw}")
raise Exception("返回碼不為200")
try:
# 返回為字典型別
return res.json()
except requests.exceptions.JSONDecodeError:
log.warning("響應引數不為json,返回響應 response物件")
return res
封裝用例資料解析
# common/data_parser.py
from config.env import get_url
from common.request_util import send_request
import jsonpath
from common.log_util import log
def parser(server_name, data_dict, param=None, **kwargs):
"""
:param server_name: env.py 中的服務域名 型別str ex: api_backend
:param data_dict: test_xxx.py測試用例對應data.py中的介面請求資料字典 ex: api_backend["get_student"]
:param param: data.py 引數化列表中一項中的data ex: api_backend["get_student"]["param_list"][0]["data"]
:**kwargs: 擴充套件引數
:return: 請求結果,如果響應是json型別返回dict,否則返回response物件
"""
# 獲取配置中的伺服器域名,拼接path
url = get_url(server_name) + data_dict["path"]
method = data_dict["method"]
headers = data_dict["headers"]
data = data_dict["data"]
# 引數化後發起請求,用引數化引數更新或替代通用引數
if param:
if isinstance(data, dict) and isinstance(param, dict):
# 如果通用引數為字典,引數化引數也為字典,使用引數化引數更新通用引數 ex: {"xx": "xx"}
data.update(param)
else:
# 如果通用引數是字串、列表(元素為字元、數字、字典),直接使用引數化引數據替換通用引數 ex: ["xx", "xx"]
data = param
res = send_request(url, method, data, headers, **kwargs)
return res
def assert_res(res_dict, expect_dict):
"""
:param res_dict: request請求返回的結果字典,型別 dict
:param expect_dict: 預期結果字典, 型別 dict
"""
if isinstance(res_dict, dict):
log.info("開始斷言")
log.info(f"預期結果: {expect_dict}")
# 遍歷預期結果的key,使用jsonpath獲取請求結果的value,與預期結果value比對
for k in expect_dict.keys():
res_list = jsonpath.jsonpath(res_dict, '$..' + str(k)) # 返回列表
assert expect_dict[k] in res_list
log.info("斷言透過")
else:
log.warning("請求結果不為dict字典型別,跳過斷言!")
封裝 faker 模擬資料
# common/faker.py
from faker import Faker
from common.log_util import log
fake = Faker("zh_CN")
def get_name():
name = fake.name()
log.info(f"faker 生成姓名: {name}")
return name
def get_phone_number():
phone_number = fake.phone_number()
log.info(f"faker 生成手機號: {phone_number}")
return phone_number
def get_id_card():
id_card = fake.ssn()
log.info(f"faker 生成身份證號: {id_card}")
return id_card
pytest.ini
[pytest]
addopts = -p no:warnings -vs
markers =
multiprocess: suppurt mutl-process execute cases
全域性 conftest.py
# conftest.py
import pytest
from common.log_util import log
from filelock import FileLock
import json
from config.env import ENV
import os
import allure
# 自定義環境資訊pytest命令列
def pytest_addoption(parser):
parser.addoption(
"--env",
action="store",
default="test",
help="set pytest running environment ex: --env=test --env=prod"
)
# 從pytest命令列獲取環境資訊
@pytest.fixture(scope="session")
def get_env(request):
ENV.info = request.config.getoption("--env")
log.info("執行環境: " + ENV.info)
return ENV.info
# 終結函式,最後執行
@pytest.fixture(scope="session", autouse=True)
def fixture_case_end(request):
def case_end():
log.info("測試結束")
request.addfinalizer(case_end)
@pytest.fixture(scope="session", autouse=True)
# fixture 巢狀先執行獲取環境資訊get_env
# 加入 tmp_path_factory worker_id 用於多程序執行 # 多程序執行,token只獲取一次
def fixture_get_token(get_env, tmp_path_factory, worker_id):
# 單程序執行
if worker_id == "master":
# 獲取token
token = {"token": "xpcs"}
log.info("fixture_get_token master獲取token %s" % token['token'])
else:
# 多程序執行
root_tmp_dir = tmp_path_factory.getbasetemp().parent
fn = root_tmp_dir / "data.json"
# 這裡with裡面的語句,理解為是被加鎖的,同一時間只能有一個程序訪問
with FileLock(str(fn) + ".lock"):
if fn.is_file():
# session_fixture 獲取token已執行,直接從檔案中讀取token
token = json.loads(fn.read_text())
log.info("fixture_get_token slave使用token %s" % token['token'])
else:
token = {"token": "xpcs"}
fn.write_text(json.dumps(token))
log.info("fixture_get_token slave獲取token %s" % token['token'])
yield token['token']
# session 結束後自動執行如下
log.info("session結束")
# 用例失敗自動執行鉤子函式
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item):
# 獲取鉤子方法的呼叫結果
outcome = yield
rep = outcome.get_result()
# 僅僅獲取用例call 執行結果是失敗的情況, 不包含 setup/teardown
if rep.when == "call" and rep.failed:
mode = "a" if os.path.exists("failures") else "w"
with open("failures", mode) as f:
# let's also access a fixture for the fun of it
if "tmpdir" in item.fixturenames:
extra = " (%s)" % item.funcargs["tmpdir"]
else:
extra = ""
f.write(rep.nodeid + extra + "\n")
with allure.step("用例執行失敗,可加入資訊"):
allure.attach("失敗內容: ----xpcs----", "失敗標題", allure.attachment_type.TEXT)
測試用例
# line_of_business/service_name_api_backend/test_api_backend.py
import pytest
from time import sleep
import allure
from common.data_parser import parser, assert_res
from config.mysql import get_mysql_conn
from common.log_util import log
from line_of_business_name.service_name_api_backend.data import api_backend
from common.faker_util import get_name, get_id_card, get_phone_number
from config.redis import get_redis_conn
@allure.feature("flask後端介面測試")
class TestApiBackend:
@classmethod
def setup_class(cls):
# 獲取資料庫連線,執行sql測試
log.info("setup_class")
# 資料庫連線根據db庫名單例,相同庫返回同一個連線
conn = get_mysql_conn("api_auto")
conn1 = get_mysql_conn("api_auto")
conn.execute_dml("insert into test_xdist(msg) values ('%s')" % "class_setup-資料庫寫入測試")
conn1.fetchone("select * from test_xdist limit 1")
@classmethod
def teardown_class(cls):
log.info("steup_teardowm")
# 獲取redis連線,執行命令測試
# redis連線根據host單例,相同host返回同一個連線
rs = get_redis_conn("api_backend")
rs1 = get_redis_conn("api_backend")
rs.set("name", "xp")
rs1.get("name")
@allure.story("測試故事1")
@pytest.mark.xfail(reason='預期失敗用例')
@user12ize("param", [{"title": "標題1", "param": 2, "assert": 3}])
def test_case_one(self, param):
sleep(1)
allure.dynamic.description("測試故事1-描述資訊")
allure.dynamic.severity(allure.severity_level.CRITICAL) # 用例級別嚴重
# allure動態標題
allure.dynamic.title(param["title"])
log.info("測試faker資料")
log.info(f"{get_name()} {get_phone_number()} {get_id_card()}")
# pytest.assume(False) # 多重斷言外掛,斷言失敗繼續執行下面
assert param["param"] + 2 == param["assert"]
@allure.story("查詢學生介面")
@user14cess # 此用例分組到可多程序跑測
@user15ize("param", api_backend["get_student"]["param_list"])
def test_get_student(self, param, fixture_get_token):
sleep(1)
allure.dynamic.title(param["title"])
data_dict = api_backend["get_student"]
data_dict["headers"]["Cookie"] = fixture_get_token
res = parser("api_backend", data_dict, param["data"])
assert_res(res, param["assert"])
@allure.story("新增學生介面")
@user17cess # 此用例分組到可多程序跑測
@user18ize("param", api_backend["post_student"]["param_list"])
def test_post_student(self, param):
sleep(1)
allure.dynamic.title(param["title"])
data_dict = api_backend["post_student"]
res = parser("api_backend", data_dict, param["data"])
assert_res(res, param["assert"])
@allure.story("更新學生介面")
@user20cess # 此用例分組到可多程序跑測
@user21ize("param", api_backend["put_student"]["param_list"])
def test_put_student(self, param):
sleep(1)
allure.dynamic.title(param["title"])
data_dict = api_backend["put_student"]
res = parser("api_backend", data_dict, param["data"])
assert_res(res, param["assert"])
用例資料驅動
# line_of_business/service_name_api_backend/data.py
# 服務名外層大字典,引數key是介面名,value是介面的請求資訊字典,用例模組可透過介面名引用介面資訊字典
# param_list 引數化列表,用於pytest引數化,每次選取其中一項的data,去更新外部data通用引數,發起請求
api_backend = {
"get_student": dict(path="/student",
method="get",
# headers 不包含Content-Type 則request使用params傳參
headers={},
# 通用引數,每次請求使用
data={"test": "test"},
# 引數化引數,每次使用其中一項,更新通用引數
param_list=[
{"title": "獲取學生資訊-張三", "data": {"name": "張三"}, "assert": {"code": 0, "msg": "ok"}},
{"title": "獲取學生資訊-李四", "data": {"name": "李四"}, "assert": {"code": 0, "msg": "ok"}},
{"title": "獲取學生資訊-王五", "data": {"name": "王五"}, "assert": {"code": 0, "msg": "ok"}}
]),
'post_student': dict(path="/student",
method="post",
# headers Content-Type = application/x-www-form-urlencoded 則使用 request使用data傳參
headers={"Cookie": "", "Content-Type": "application/x-www-form-urlencoded"},
data={"test": "test"},
param_list=[
{"title": "新增學生資訊-張三", "data": {"name": "張三"}, "assert": {"code": 1, "msg": "ok"}},
{"title": "新增學生資訊-李四", "data": {"name": "李四"}, "assert": {"code": 0, "msg": "ok"}},
{"title": "新增學生資訊-王五", "data": {"name": "王五"}, "assert": {"code": 0, "msg": "ok"}}
]),
'put_student': dict(path="/student",
method="put",
# headers Content-Type = application/json 則使用 request使用json傳參
headers={"Cookie": "", "Content-Type": "application/json"},
data={"test": "test"},
param_list=[
{"title": "更新學生資訊-張三", "data": {"name": "張三"}, "assert": {"code": 0, "msg": "ok"}},
{"title": "更新學生資訊-李四", "data": {"name": "李四"}, "assert": {"code": 0, "msg": "okk"}},
{"title": "更新學生資訊-王五", "data": {"name": "王五"}, "assert": {"code": 0, "msg": "ok"}}
])
}
除錯執行入口
# run.py
import pytest
import os
# 用例除錯入口
if __name__ == '__main__':
pytest.main([r"line_of_business_name", "--clean-alluredir", "--alluredir=allure_result", "--cache-clear", "--env=prod"])
# pytest.main([r"-m multiprocess", "--clean-alluredir", "--alluredir=allure_result", "-n 3", "--cache-clear", "--env=prod"])
os.system(r"allure generate allure_result -c -o allure_report")
os.system(r"allure open -h 127.0.0.1 -p 8899 allure_report")
失敗用例重跑
# failed_run.py
import pytest
import os
# 失敗用例重跑
if __name__ == '__main__':
pytest.main([r"line_of_business_name", "--lf", "--clean-alluredir", "--alluredir=allure_result", "--env=prod"])
os.system(r"allure generate allure_result -c -o allure_report")
os.system(r"allure open -h 127.0.0.1 -p 8899 allure_report")
報告展示
相關文章
- 介面自動化(四):框架搭建(Python)框架Python
- 介面自動化實戰之框架搭建框架
- 介面自動化測試框架搭建總結框架
- 介面自動化測試框架搭建的思路框架
- Jmeter+Ant+Jenkins介面自動化測試框架搭建for WindowsJMeterJenkins框架Windows
- Java語言搭建介面自動化框架學習八(鑑權)Java框架
- 介面自動化測試框架 HttpFPT框架HTTP
- Java語言搭建介面自動化框架學習一(單介面請求和響應)Java框架
- 四.unittest介面自動化框架介紹框架
- 介面自動化測試工程實踐分享
- 【python介面自動化】初識unittest框架Python框架
- Jmeter+Ant+Jenkins介面自動化框架JMeterJenkins框架
- 常用網路介面自動化測試框架框架
- 基於Python+requests搭建的自動化框架-實現流程化的介面串聯Python框架
- Jmeter+Ant+Jenkins介面自動化框架(續)JMeterJenkins框架
- 搭建 WPF 上的 UI 自動化測試框架UI框架
- Linux下搭建介面自動化測試平臺Linux
- Httpclient 介面自動化HTTPclient
- 全自動化介面
- Keil中搭建自動化單元測試框架Unity框架Unity
- 介面自動化與ui自動化區別UI
- Python+Pytest+Allure+Jenkins 介面自動化框架PythonJenkins框架
- 基於Python3.7 Robot Framework自動化框架搭建PythonFramework框架
- 介面自動化測試
- python 介面自動化Python
- 自動化測試框架框架
- Python+Pytest+Allure+Git+Jenkins介面自動化框架PythonGitJenkins框架
- 介面自動化測試PHPUnit-框架程式碼開發3PHP框架
- 介面自動化測試PHPUnit-框架程式碼開發1PHP框架
- 介面自動化之介面整理(抓包)
- JMeter 介面自動化測試(手工轉自動化指令碼)JMeter指令碼
- 利用tox打造自動自動化測試框架框架
- <討論>2020年 的 python 介面自動化框架Python框架
- 關於介面測試——自動化框架的設計與實現框架
- 介面自動化測試之PHPUnit-框架程式碼編寫2PHP框架
- Python + requests + unittest + ddt 進行介面自動化測試的框架Python框架
- python介面自動化測試 —— unittest框架suite、runner詳細使用Python框架UI
- titans Selenium 自動化框架框架