接口自动化测试全流程:pytest 用例收集、并行执行、Allure 报告合并与上传

接口自动化测试全流程: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 (手动)

  1. 下载 https://github.com/allure-framework/allure2/releases 中的 allure-<version>.zip
  2. 解压到 D:\allure
  3. 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 ./reportpython -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.jsoncontainer.jsonenvironment.properties)解析
  • ✅ 报告打包上传至远程 Flask 服务
  • ✅ 完整的一键运行脚本

这套流程已广泛应用于生产环境的接口回归测试,支持数千条用例的每日执行。你可以根据实际需求调整并行数、重试策略及上传目标,轻松集成到 Jenkins、GitLab CI 等流水线中。

参考资料

所有路径、IP、域名均为示例,请替换为你实际使用的值。如果在实践中遇到问题,欢迎留言交流!

相关推荐
chushiyunen3 小时前
python fastapi使用、uvicorn
开发语言·python·fastapi
咕白m6253 小时前
Python 高效添加与管理 Excel 工作表
后端·python
pixle04 小时前
【 LangChain v1.2 入门系列教程】【四】结构化输出,让 Agent 返回可预测的结构
python·ai·langchain·agent·智能体
木心术14 小时前
openclaw与Hermes的优劣势对比
人工智能·python·opencv·自动化
潇洒畅想4 小时前
1.2 希腊字母速查表 + 公式阅读实战
java·人工智能·python·算法·rust·云计算
深度学习lover4 小时前
<数据集>yolo 瓶盖识别<目标检测>
人工智能·python·yolo·计算机视觉·瓶盖识别
测绘第一深情4 小时前
MapQR:自动驾驶在线矢量化高精地图构建的端到端 SOTA 方法
数据结构·人工智能·python·神经网络·算法·机器学习·自动驾驶
高洁014 小时前
AI算法实战:逻辑回归在风控场景中的应用
人工智能·python·深度学习·transformer
书香门第5 小时前
搭建免费的Ollama AI Agent
人工智能·python·ollama