接口自动化测试全流程:pytest 用例收集、并行执行、Allure 报告合并与上传
本文将带你从零开始构建一套完整的接口自动化测试体系。涵盖:Allure 安装、pytest 基础与高级用法、用例筛选(模块/标签/优先级)、失败重试与超时控制、丰富的 Allure 附件(日志、截图、录屏、文件夹打包)、多进程并行执行、子报告合并、最终生成统一 HTML 报告并上传至远程服务。所有代码均可直接用于生产实践。
1. Allure 报告安装指南
Allure 是一款开源的测试报告框架,能将测试数据渲染为美观的 HTML 报告。安装 Allure 命令行工具是生成报告的前提。
1.1 前置条件
Allure 依赖 Java 运行环境,需 JDK 8+。
bash
java -version
1.2 各平台安装方式
macOS
bash
brew install allure
Windows (Scoop)
bash
scoop install allure
Windows (手动)
- 下载 https://github.com/allure-framework/allure2/releases 中的
allure-<version>.zip - 解压到
D:\allure - 将
D:\allure\bin加入系统 PATH
Linux (Ubuntu/Debian)
bash
sudo apt-add-repository ppa:qameta/allure
sudo apt update
sudo apt install allure
1.3 验证安装
bash
allure --version
1.4 安装 Python 依赖
bash
pip install pytest allure-pytest requests
# 并行执行与失败重试插件
pip install pytest-xdist pytest-rerunfailures pytest-timeout
2. 项目结构
text
/home/tester/api_test/
├── test_cases/
│ ├── user_module/
│ │ ├── test_login.py
│ │ └── test_profile.py
│ └── order_module/
│ └── test_order.py
├── conftest.py
├── utils/
│ └── attach_helper.py
├── pytest.ini
└── requirements.txt
/home/tester/report_workspace/
├── raw_results/ # 并行执行时各进程的原始结果
├── merged_results/ # 合并后的结果目录
├── final_report/ # 最终 Allure HTML 报告
└── logs/ # 运行日志
3. pytest 基础与自定义参数
3.1 常用运行参数
| 参数 | 作用 | 示例 |
|---|---|---|
-v |
显示详细信息 | pytest -v |
-s |
允许 print 输出 | pytest -s |
-k |
按表达式匹配用例名称 | pytest -k "login" |
-m |
运行带特定标记的用例 | pytest -m "smoke" |
--tb=short |
精简回溯信息 | pytest --tb=short |
--maxfail=1 |
遇到第一个失败即停止 | pytest --maxfail=1 |
--collect-only |
仅收集用例,不执行 | pytest --collect-only -q |
--alluredir=./results |
指定 Allure 原始数据输出目录 | pytest --alluredir=./my_results |
--clean-alluredir |
执行前清空 alluredir | pytest --alluredir=./results --clean-alluredir |
3.2 自定义命令行参数(conftest.py)
python
# conftest.py
import pytest
def pytest_addoption(parser):
parser.addoption(
"--env",
action="store",
default="test",
choices=["test", "staging", "production"],
help="运行环境"
)
parser.addoption(
"--timeout",
action="store",
default=30,
type=int,
help="接口请求超时(秒)"
)
parser.addoption(
"--base-url",
action="store",
default="https://api.example.com",
help="基础URL"
)
@pytest.fixture(scope="session")
def env(request):
return request.config.getoption("--env")
4. 批量运行时的用例筛选
4.1 按模块目录筛选
bash
# 运行整个模块
pytest test_cases/user_module/
# 运行单个文件
pytest test_cases/user_module/test_login.py
# 运行特定类
pytest test_cases/user_module/test_login.py::TestLogin
4.2 按标签(mark)筛选
在用例上定义标签:
python
import pytest
@pytest.mark.smoke
@pytest.mark.regression
def test_login():
pass
@pytest.mark.slow
def test_heavy():
pass
运行命令:
bash
pytest -m smoke # 冒烟测试
pytest -m "regression and not smoke" # 回归但不包含冒烟
pytest -m "smoke or slow" # 冒烟或慢速
4.3 按优先级筛选(自定义标记)
python
@pytest.mark.p0 # 最高优先级
def test_critical():
pass
@pytest.mark.p1
def test_important():
pass
运行:
bash
pytest -m "p0 or p1"
4.4 按名称关键词筛选
bash
pytest -k "login" # 名称包含 login
pytest -k "login and not admin" # 逻辑组合
4.5 组合使用
bash
pytest test_cases/user_module/ -m smoke -k "login"
4.6 pytest.ini 配置默认标记
ini
[pytest]
markers =
smoke: 冒烟测试
regression: 回归测试
p0: 最高优先级
p1: 高优先级
slow: 执行较慢
5. 运行中的失败与超时处理
5.1 失败重试(pytest-rerunfailures)
bash
# 失败后重试 2 次,间隔 1 秒
pytest --reruns 2 --reruns-delay 1
在用例级别指定重试:
python
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_unstable():
pass
5.2 用例超时控制(pytest-timeout)
bash
# 全局超时 300 秒
pytest --timeout=300
在用例级别指定超时:
python
@pytest.mark.timeout(60)
def test_long_running():
pass
5.3 失败时自动截图/保存页面源码(UI 测试示例)
在 conftest.py 中添加钩子:
python
import allure
from selenium import webdriver
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
if rep.when == "call" and rep.failed:
driver = item.funcargs.get('driver', None)
if driver:
screenshot = driver.get_screenshot_as_png()
allure.attach(screenshot, name="失败截图", attachment_type=allure.attachment_type.PNG)
html = driver.page_source
allure.attach(html, name="页面源码", attachment_type=allure.attachment_type.HTML)
5.4 失败时收集完整日志
python
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
if rep.when == "call" and rep.failed:
log_file = item.funcargs.get('log_file_path', None)
if log_file and os.path.exists(log_file):
with open(log_file, 'r') as f:
allure.attach(f.read(), name="运行日志", attachment_type=allure.attachment_type.TEXT)
6. Allure 报告附件高级用法
Allure 支持在报告中附加任意格式的文件,极大提升问题定位效率。
6.1 基础附件类型
| 类型 | 枚举值 | 场景 |
|---|---|---|
| 纯文本 | allure.attachment_type.TEXT |
日志、说明 |
| JSON | allure.attachment_type.JSON |
接口响应 |
| PNG | allure.attachment_type.PNG |
截图 |
| OTHER | allure.attachment_type.OTHER |
视频、压缩包 |
6.2 附加文本与 JSON
python
import allure
import requests
def test_api():
response = requests.get("https://api.example.com/user/1")
allure.attach("测试备注", name="说明", attachment_type=allure.attachment_type.TEXT)
allure.attach(str(response.json()), name="响应JSON", attachment_type=allure.attachment_type.JSON)
assert response.status_code == 200
6.3 附加日志文件(.log)
python
def test_with_log():
with open("/var/log/my_test.log", 'r') as f:
allure.attach(f.read(), name="运行日志", attachment_type=allure.attachment_type.TEXT)
assert True
6.4 附加截图(PNG)
python
def test_screenshot():
# 假设 screenshot_bytes 为 PNG 格式的字节流
screenshot_bytes = driver.get_screenshot_as_png()
allure.attach(screenshot_bytes, name="页面截图", attachment_type=allure.attachment_type.PNG)
6.5 附加录屏(MP4)
python
def test_video():
with open("/path/to/recording.mp4", "rb") as f:
allure.attach(f.read(), name="测试录屏", attachment_type=allure.attachment_type.OTHER, extension=".mp4")
6.6 附加整个文件夹(打包为 ZIP)
python
import zipfile, os
def test_zip_folder():
folder = "./test_artifacts"
zip_path = "/tmp/artifacts.zip"
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(folder):
for file in files:
zf.write(os.path.join(root, file), file)
with open(zip_path, "rb") as f:
allure.attach(f.read(), name="测试产物", attachment_type=allure.attachment_type.OTHER, extension=".zip")
os.remove(zip_path)
6.7 封装辅助函数(utils/attach_helper.py)
python
import allure, json, zipfile, os
def attach_text(content, name="text"):
allure.attach(content, name, allure.attachment_type.TEXT)
def attach_json(data, name="json"):
if isinstance(data, dict):
data = json.dumps(data, indent=2)
allure.attach(data, name, allure.attachment_type.JSON)
def attach_file(file_path, name=None):
with open(file_path, "rb") as f:
allure.attach(f.read(), name or os.path.basename(file_path), extension=os.path.splitext(file_path)[1])
def attach_screenshot(screenshot_bytes, name="screenshot"):
allure.attach(screenshot_bytes, name, allure.attachment_type.PNG)
def attach_zip_of_folder(folder_path, zip_name="archive.zip", display_name="Folder"):
zip_path = f"/tmp/{zip_name}"
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, _, files in os.walk(folder_path):
for file in files:
full = os.path.join(root, file)
zf.write(full, os.path.relpath(full, folder_path))
with open(zip_path, "rb") as f:
allure.attach(f.read(), display_name, allure.attachment_type.OTHER, extension=".zip")
os.remove(zip_path)
7. 并行执行与子报告生成(pytest-xdist)
7.1 原理
pytest-xdist通过-n参数启动多个 worker 进程。- 每个 worker 独立执行部分用例,并生成独立的
allure-results文件。 - 通过
--alluredir={}模板为每个 worker 指定不同输出目录,避免覆盖。
7.2 脚本实现
bash
#!/bin/bash
set -e
WORK_DIR="/home/tester/api_test"
ENV_PATH="/home/tester/venv/bin/activate"
CASE_PATH="test_cases"
PARALLEL_NUM=4
BASE_RESULT_DIR="/home/tester/report_workspace/raw_results"
cd ${WORK_DIR}
source ${ENV_PATH}
rm -rf ${BASE_RESULT_DIR}
mkdir -p ${BASE_RESULT_DIR}
for i in $(seq 0 $((PARALLEL_NUM-1))); do
mkdir -p "${BASE_RESULT_DIR}/worker_${i}"
done
pytest ${CASE_PATH} \
-n ${PARALLEL_NUM} \
--dist loadscope \
--env=test \
--alluredir=${BASE_RESULT_DIR}/worker_{} \
--clean-alluredir \
-v --tb=short \
--reruns 2 --reruns-delay 1 \
--timeout=300
7.3 合并子报告
bash
MERGED_DIR="/home/tester/report_workspace/merged_results"
mkdir -p ${MERGED_DIR}
for worker_dir in ${BASE_RESULT_DIR}/worker_*; do
cp -rf ${worker_dir}/* ${MERGED_DIR}/ 2>/dev/null || true
done
8. 生成最终 Allure 报告
bash
FINAL_REPORT_DIR="/home/tester/report_workspace/final_report/$(date +%Y%m%d%H%M%S)"
mkdir -p ${FINAL_REPORT_DIR}
allure generate ${MERGED_DIR} -o ${FINAL_REPORT_DIR} --clean
echo "报告生成于: ${FINAL_REPORT_DIR}"
8.1 Allure 命令行参数
| 命令 | 作用 |
|---|---|
allure generate ./results -o ./report --clean |
生成 HTML 报告 |
allure open ./report |
本地打开报告 |
allure serve ./results |
启动临时服务查看原始结果 |
9. 报告关键文件解析
执行 pytest --alluredir=./allure-results 后,目录下会生成:
| 文件类型 | 示例 | 内容 |
|---|---|---|
-result.json |
abc123-result.json |
每个用例的详细信息:名称、状态、步骤、附件引用 |
-container.json |
def456-container.json |
测试类/模块的容器,记录 setup/teardown |
| 附件文件 | uuid-attachment.png |
实际存储的图片、日志等 |
environment.properties |
可选 | 环境变量:OS、Python版本、BaseURL |
categories.json |
可选 | 自定义缺陷分类规则 |
示例 -result.json 片段:
json
{
"name": "test_login_success",
"status": "passed",
"labels": [{"name": "feature", "value": "登录"}],
"steps": [{"name": "发送请求", "status": "passed"}],
"attachments": [{"source": "xyz-attachment.json", "type": "application/json"}]
}
环境信息配置 :在 allure-results 目录下创建 environment.properties:
properties
OS=Ubuntu 20.04
Python=3.9.7
Allure=2.34.1
BaseURL=https://test.api.example.com
缺陷分类配置 :categories.json
json
[
{"name": "接口超时", "matchedStatuses": ["broken"], "messageRegex": ".*timeout.*"},
{"name": "产品缺陷", "matchedStatuses": ["failed"]}
]
10. 打包上传至远程服务
10.1 压缩报告
bash
cd $(dirname ${FINAL_REPORT_DIR})
zip -r "${TIMESTAMP}.zip" "$(basename ${FINAL_REPORT_DIR})"
10.2 上传到 Flask 服务器
bash
SERVER_IP="192.168.1.100"
SERVER_PORT="8080"
SERVER_HOSTNAME="report-server.example.com"
curl -F "report=@${TIMESTAMP}.zip" http://${SERVER_IP}:${SERVER_PORT}/upload
REPORT_URL="http://${SERVER_HOSTNAME}:${SERVER_PORT}/reports/${TIMESTAMP}"
echo "报告地址: ${REPORT_URL}"
10.3 Flask 服务端示例
python
from flask import Flask, request, redirect, send_from_directory
import os, zipfile
app = Flask(__name__)
UPLOAD_DIR = "/var/www/reports"
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['report']
timestamp = file.filename.replace('.zip', '')
save_path = os.path.join(UPLOAD_DIR, timestamp)
os.makedirs(save_path, exist_ok=True)
zip_path = os.path.join(save_path, file.filename)
file.save(zip_path)
with zipfile.ZipFile(zip_path, 'r') as zf:
zf.extractall(save_path)
os.remove(zip_path)
return redirect(f"/reports/{timestamp}/index.html")
@app.route('/reports/<path:path>')
def serve_report(path):
return send_from_directory(UPLOAD_DIR, path)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
11. 完整自动化脚本
run_full_api_test.sh:
bash
#!/bin/bash
set -e
# ========== 配置 ==========
WORK_DIR="/home/tester/api_test"
ENV_PATH="/home/tester/venv/bin/activate"
CASE_PATH="test_cases"
PARALLEL_NUM=4
BASE_RESULT_DIR="/home/tester/report_workspace/raw_results"
MERGED_DIR="/home/tester/report_workspace/merged_results"
REPORT_BASE="/home/tester/report_workspace/final_report"
LOG_DIR="/home/tester/report_workspace/logs"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
FINAL_REPORT_DIR="${REPORT_BASE}/${TIMESTAMP}"
LOG_FILE="${LOG_DIR}/exec_${TIMESTAMP}.log"
SERVER_IP="192.168.1.100"
SERVER_PORT="8080"
SERVER_HOSTNAME="report-server.example.com"
# ========== 环境准备 ==========
mkdir -p ${LOG_DIR} ${BASE_RESULT_DIR} ${REPORT_BASE}
cd ${WORK_DIR}
source ${ENV_PATH}
echo "[$(date)] 开始执行,日志: ${LOG_FILE}" | tee -a ${LOG_FILE}
rm -rf ${BASE_RESULT_DIR} ${MERGED_DIR}
mkdir -p ${BASE_RESULT_DIR} ${MERGED_DIR}
for i in $(seq 0 $((PARALLEL_NUM-1))); do
mkdir -p "${BASE_RESULT_DIR}/worker_${i}"
done
# ========== 1. 收集用例 ==========
echo "[$(date)] 收集用例列表..." | tee -a ${LOG_FILE}
pytest ${CASE_PATH} --collect-only -q | tee -a ${LOG_FILE}
# ========== 2. 并行执行 ==========
echo "[$(date)] 启动 ${PARALLEL_NUM} 个进程..." | tee -a ${LOG_FILE}
pytest ${CASE_PATH} \
-n ${PARALLEL_NUM} \
--dist loadscope \
--env=test \
--alluredir=${BASE_RESULT_DIR}/worker_{} \
--clean-alluredir \
-v --tb=short \
--reruns 2 --reruns-delay 1 \
--timeout=300 2>&1 | tee -a ${LOG_FILE}
PYTEST_EXIT_CODE=${PIPESTATUS[0]}
# ========== 3. 合并结果 ==========
echo "[$(date)] 合并子报告..." | tee -a ${LOG_FILE}
for worker_dir in ${BASE_RESULT_DIR}/worker_*; do
cp -rf ${worker_dir}/* ${MERGED_DIR}/ 2>/dev/null || true
done
# 添加环境信息(可选)
if [ -f "${WORK_DIR}/environment.properties" ]; then
cp "${WORK_DIR}/environment.properties" ${MERGED_DIR}/
fi
# ========== 4. 生成报告 ==========
echo "[$(date)] 生成 Allure 报告..." | tee -a ${LOG_FILE}
mkdir -p ${FINAL_REPORT_DIR}
allure generate ${MERGED_DIR} -o ${FINAL_REPORT_DIR} --clean
echo "[$(date)] 报告生成: ${FINAL_REPORT_DIR}" | tee -a ${LOG_FILE}
# ========== 5. 打包上传 ==========
echo "[$(date)] 打包并上传报告..." | tee -a ${LOG_FILE}
cd $(dirname ${FINAL_REPORT_DIR})
zip -r "${TIMESTAMP}.zip" "${TIMESTAMP}" >> ${LOG_FILE} 2>&1
for i in {1..3}; do
curl -F "report=@${TIMESTAMP}.zip" http://${SERVER_IP}:${SERVER_PORT}/upload 2>&1 | tee -a ${LOG_FILE}
if [ $? -eq 0 ]; then
break
fi
sleep 5
done
REPORT_URL="http://${SERVER_HOSTNAME}:${SERVER_PORT}/reports/${TIMESTAMP}"
echo "==========================================" | tee -a ${LOG_FILE}
echo "测试执行完毕。报告地址:" | tee -a ${LOG_FILE}
echo "${REPORT_URL}" | tee -a ${LOG_FILE}
echo "==========================================" | tee -a ${LOG_FILE}
exit ${PYTEST_EXIT_CODE}
使用方法:
bash
chmod +x run_full_api_test.sh
./run_full_api_test.sh
12. 常见问题与解决方案
| 问题 | 解决方案 |
|---|---|
allure: command not found |
检查 Allure 是否正确安装并加入 PATH |
直接打开 index.html 显示空白 |
需启动 HTTP 服务:allure open ./report 或 python -m http.server |
| 并行执行时附件丢失 | 合并时确保复制所有 worker 目录下的文件(含附件) |
| 报告体积过大 | 限制截图数量,失败时才截图;服务器端定期清理旧报告 |
| 网络波动导致上传失败 | 脚本已内置重试 3 次 |
| 用例执行时间过长 | 使用 pytest-timeout 设置全局或单个超时 |
| 偶发失败影响整体结果 | 使用 pytest-rerunfailures 重试 |
13. 总结
通过本文,你已经完整掌握:
- ✅ Allure 命令行工具的安装(macOS/Windows/Linux)
- ✅ pytest 基础与自定义参数(
--env、--timeout等) - ✅ 批量运行时的用例筛选:模块、标签(
-m)、关键词(-k)、优先级 - ✅ 失败处理:重试(
reruns)、超时(timeout)、失败截图/日志收集 - ✅ Allure 附件高级用法:文本、JSON、日志、截图、录屏、打包文件夹
- ✅ 并行执行(
pytest-xdist)与多进程结果合并 - ✅ 生成最终 HTML 报告及关键文件(
-result.json、container.json、environment.properties)解析 - ✅ 报告打包上传至远程 Flask 服务
- ✅ 完整的一键运行脚本
这套流程已广泛应用于生产环境的接口回归测试,支持数千条用例的每日执行。你可以根据实际需求调整并行数、重试策略及上传目标,轻松集成到 Jenkins、GitLab CI 等流水线中。
参考资料
所有路径、IP、域名均为示例,请替换为你实际使用的值。如果在实践中遇到问题,欢迎留言交流!