接口自动化测试全流程: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、域名均为示例,请替换为你实际使用的值。如果在实践中遇到问题,欢迎留言交流!

相关推荐
老白干8 分钟前
jjwt 0.9.1 在 JDK 11+ 上的两个“坑”与完整解决方案
java·python·log4j
笨鸟先飞,勤能补拙7 小时前
AI 赋能网络安全:技术全景、成熟度评估与实战案例
人工智能·python·安全·web安全·网络安全·sqlite·github
长和信泰光伏储能7 小时前
京津冀光伏发电:绿色能源的未来之路
python·能源
浦信仿真大讲堂8 小时前
从重复操作到自动化闭环:如何让 CST 与 Python 真正协同起来
python·自动化·cst·仿真软件·达索软件
Gu Gu Study8 小时前
ScoutLoop开放域深度研究引擎(agent的初步设计想法)
人工智能·python
卷无止境9 小时前
写代码这件事,到底该讲究点什么?
后端·python
卷无止境9 小时前
循环复杂度到底在算什么,Python 代码怎么才能写得让人一看就懂
后端·python
lpfasd1239 小时前
MediaCrawler 项目深度分析
chrome·python·chrome devtools
Dxy123931021610 小时前
Python项目打包成EXE完整教程(PyInstaller实战避坑)
开发语言·python
bamb0010 小时前
一个项目带你入门AI应用开发01
python