【测试自动化】pytest + Allure 完整学习指南

基于 allure-pytest 2.13+ | Python 3.8+ | Allure 2.x | Windows 10/11


目录

  1. 安装与环境搭建
  2. 运行测试与生成报告
  3. 描述类注解
  4. 分类与层级标签(Epic / Feature / Story)
  5. 严重程度(Severity)
  6. 链接注解(Link / Issue / TmsLink)
  7. 测试步骤(Step)
  8. 附件(Attachment)
  9. 参数化测试
  10. 动态更新(allure.dynamic)
  11. Fixture 与 Allure 集成
  12. conftest.py 全局配置
  13. 环境信息配置
  14. 失败分类配置(Categories)
  15. 历史趋势
  16. 测试筛选与过滤
  17. 完整项目示例
  18. 常见问题与排查
  19. 速查表

1. 安装与环境搭建

1.1 安装 Python 依赖

bash 复制代码
pip install pytest allure-pytest

# 验证安装
pip show allure-pytest
pip show pytest

1.2 安装 Allure CLI(Windows)

Allure CLI 依赖 Java,需先安装 JDK。

第一步:安装 Java(JDK 8 或以上)

前往 https://www.oracle.com/java/technologies/downloads/ 下载安装,或使用 OpenJDK:

powershell 复制代码
# 验证 Java 是否安装成功
java -version
# 输出类似:java version "17.0.9" 2023-10-17 LTS

第二步:安装 Allure CLI

方式一:使用 Scoop(推荐,自动配置 PATH)

powershell 复制代码
# 先安装 Scoop(如未安装)
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression

# 安装 Allure
scoop install allure

方式二:手动安装

  1. 前往 https://github.com/allure-framework/allure2/releases 下载最新的 allure-2.x.x_windows.zip
  2. 解压到指定目录,例如 C:\allure-2.27.0
  3. C:\allure-2.27.0\bin 添加到系统环境变量 PATH
    • 右键"此电脑" → 属性 → 高级系统设置 → 环境变量
    • 在"系统变量"中找到 Path,点击编辑 → 新建 → 输入 C:\allure-2.27.0\bin
    • 确定保存,重启 PowerShell / CMD

第三步:验证安装

powershell 复制代码
allure --version
# 输出类似:2.27.0

1.3 项目结构

复制代码
my_project/
├── tests/
│   ├── conftest.py
│   ├── test_login.py
│   ├── test_cart.py
│   └── api/
│       └── test_user_api.py
├── utils/
│   └── helpers.py
├── allure-results/        # 自动生成,存放 JSON 结果
├── allure-report/         # 自动生成,存放 HTML 报告
├── categories.json        # 失败分类配置
├── environment.properties # 环境信息配置
├── pytest.ini
└── requirements.txt

1.4 pytest.ini 基础配置

复制代码
[pytest]
addopts=
    --alluredir=allure-results
    --clean-alluredir
testpaths= tests

--alluredir:指定结果文件输出目录

  • -clean-alluredir:每次运行前清空该目录,避免旧结果混入

也支持 pyproject.toml

toml 复制代码
[tool.pytest.ini_options]
addopts = "--alluredir=allure-results --clean-alluredir"
testpaths = ["tests"]

2. 运行测试与生成报告

2.1 工作原理

复制代码
pytest 运行  →  allure-results/*.json  →  allure generate  →  allure-report/index.html
  1. pytest 运行时,allure-pytest 插件会将每个测试的结果写入 allure-results 目录(JSON 格式)
  2. allure generate 命令读取这些 JSON,生成静态 HTML 报告

2.2 常用命令(PowerShell / CMD)

powershell 复制代码
# 运行所有测试,结果输出到 allure-results
pytest tests\

# 指定结果目录(不使用 pytest.ini 时)
pytest tests\ --alluredir=allure-results

# 每次运行前清空旧结果
pytest tests\ --alluredir=allure-results --clean-alluredir

# 生成 HTML 报告
allure generate allure-results -o allure-report --clean

# 用浏览器打开已生成的报告(会自动启动本地服务)
allure open allure-report

# 【推荐,开发调试】一步到位:生成报告并立即打开
allure serve allure-results

# 指定端口和地址
allure serve allure-results -p 9999 -h 127.0.0.1

⚠️ 不要直接双击 index.html 打开,浏览器会因 CORS 限制显示空白页。

必须通过 allure openallure serve 命令启动本地服务后访问。

2.3 一键脚本(Windows .bat)

在项目根目录创建 run_test.bat

bash 复制代码
@echo off
echo ========== 清空旧结果 ==========
if exist allure-results rmdir /s /q allure-results
if exist allure-report  rmdir /s /q allure-report

echo ========== 运行测试 ==========
pytest tests\ --alluredir=allure-results

echo ========== 生成报告 ==========
allure generate allure-results -o allure-report --clean

echo ========== 打开报告 ==========
allure open allure-report

pause

双击即可完成"运行测试 → 生成报告 → 打开浏览器"全流程。

2.4 报告目录结构

复制代码
allure-results/           ← pytest 生成(原始数据)
├── abc123-result.json    ← 每个测试用例的详细数据
├── xyz456-container.json ← 测试套件/Fixture 的数据
├── attach-001.png        ← 附件文件
├── environment.properties
└── categories.json

allure-report/            ← allure generate 生成(静态网站)
├── index.html            ← 入口(必须通过服务访问)
├── data/
├── history/
└── widgets/

3. 描述类注解

这类注解用于为测试用例添加可读性信息,显示在报告的测试详情页。

3.1 @allure.title --- 测试标题

默认情况下报告使用函数名作为标题,@allure.title 可以让标题更具业务含义。

python 复制代码
import allure

# 静态标题
@allure.title("用户使用正确密码登录成功")
def test_login_with_correct_password():
    pass

# 标题中引用参数(配合参数化使用)
@allure.title("用户{username} 登录测试")
@pytest.mark.parametrize("username, password", [
    ("admin", "admin123"),
    ("user01", "pass456"),
])
def test_login_parametrize(username, password):
    pass
    # 报告中标题会变成:
    # "用户 admin 登录测试"
    # "用户 user01 登录测试"

3.2 @allure.description --- 测试描述

python 复制代码
# 方式一:装饰器
@allure.description("""
    测试场景:用户登录功能
    前置条件:用户账号已注册且处于正常状态
    测试步骤:
      1. 打开登录页面
      2. 输入正确的用户名和密码
      3. 点击登录按钮
    预期结果:页面跳转到首页,显示欢迎信息
""")
def test_user_login():
    pass

# 方式二:直接写函数的 docstring(更简洁)
def test_user_logout():
    """
    测试用户正常退出登录。
    退出后应跳转到登录页,且 session 失效。
    """
    pass

3.3 @allure.description_html --- 富文本描述

python 复制代码
@allure.description_html("""
<h3>接口测试说明</h3>
<table border="1" cellpadding="5">
  <tr><th>字段</th><th>说明</th></tr>
  <tr><td>username</td><td>用户名,长度 4-20</td></tr>
  <tr><td>password</td><td>密码,长度 6-16</td></tr>
</table>
<p><b>注意:</b>密码字段不可为空</p>
""")
def test_login_api_fields():
    pass

4. 分类与层级标签

这是 Allure 最核心的功能之一,使报告支持按业务维度导航(Behaviors 视图)。

4.1 层级结构

复制代码
@allure.epic       ← 史诗(最顶层,对应业务线/产品)
  └── @allure.feature  ← 功能模块
        └── @allure.story  ← 用户故事(具体场景)
              └── 测试用例

4.2 基本用法

python 复制代码
import allure
import pytest

@allure.epic("电商平台")
@allure.feature("用户中心")
@allure.story("账号登录")
def test_login_success():
    pass

@allure.epic("电商平台")
@allure.feature("用户中心")
@allure.story("账号注册")
def test_register():
    pass

@allure.epic("电商平台")
@allure.feature("商品管理")
@allure.story("商品搜索")
def test_search_product():
    pass

4.3 类级别标注(推荐)

python 复制代码
@allure.epic("电商平台")
@allure.feature("购物车")
class TestShoppingCart:

    @allure.story("添加商品")
    def test_add_item(self):
        pass

    @allure.story("删除商品")
    def test_remove_item(self):
        pass

    @allure.story("修改数量")
    def test_update_quantity(self):
        pass

    @allure.story("清空购物车")
    def test_clear_cart(self):
        pass

4.4 @allure.tag --- 自定义标签

python 复制代码
@allure.tag("smoke")
@allure.tag("regression")
@allure.tag("P0")
def test_core_login():
    pass

# 一次添加多个 tag
@allure.tag("smoke", "regression", "cross-browser")
def test_homepage_load():
    pass

4.5 @allure.label --- 自定义标签键值对

python 复制代码
# 自定义任意维度的标签
@allure.label("layer", "api")          # 测试层级:api / ui / unit
@allure.label("owner", "张三")         # 负责人
@allure.label("component", "login")   # 组件
def test_login_api():
    pass

5. 严重程度(Severity)

5.1 五种级别

级别 说明 使用场景示例
BLOCKER 阻断级,系统无法使用 登录接口报 500、页面打不开
CRITICAL 严重,核心功能异常 支付失败、订单创建失败
NORMAL 普通(默认值),功能性缺陷 数据展示不完整
MINOR 次要,不影响主流程 按钮样式偏移、文字截断
TRIVIAL 轻微,几乎无影响 错别字、提示语不规范

5.2 用法示例

python 复制代码
import allure

@allure.severity(allure.severity_level.BLOCKER)
def test_app_can_start():
    """应用能否正常启动"""
    pass

@allure.severity(allure.severity_level.CRITICAL)
def test_user_payment():
    """用户支付核心流程"""
    pass

@allure.severity(allure.severity_level.NORMAL)
def test_user_profile_update():
    """用户资料更新"""
    pass

@allure.severity(allure.severity_level.MINOR)
def test_button_style():
    """按钮样式校验"""
    pass

@allure.severity(allure.severity_level.TRIVIAL)
def test_footer_copyright_text():
    """页脚版权文字校验"""
    pass

6. 链接注解

6.1 三种链接类型

python 复制代码
import allure

# 通用链接(任意 URL)
@allure.link("https://example.com/docs/login", name="登录接口文档")
def test_login_api():
    pass

# 缺陷链接(对应 Bug 追踪系统)
@allure.issue("BUG-1234", "登录页面验证码不显示")
def test_captcha_display():
    pass

# 测试用例管理链接(对应 TMS 系统)
@allure.testcase("TC-5678", "登录功能正向测试用例")
def test_login_positive():
    pass

# 组合使用
@allure.link("https://wiki.example.com/login", name="需求文档")
@allure.issue("BUG-2001")
@allure.testcase("TC-1001")
@allure.severity(allure.severity_level.CRITICAL)
def test_login_full():
    pass

6.2 配置链接模板(自动拼接 URL)

pytest.ini 中配置模板,这样 @allure.issue("BUG-123") 会自动生成完整链接:

复制代码
[pytest]
addopts= --alluredir=allure-results --clean-alluredir
allure_link_pattern=
    issue:https://jira.yourcompany.com/browse/{}
    tms:https://testrail.yourcompany.com/index.php?/cases/view/{}

配置后,代码中只需写 ID,报告会自动补全 URL:

python 复制代码
@allure.issue("BUG-1234")      # → https://jira.yourcompany.com/browse/BUG-1234
@allure.testcase("TC-5678")    # → https://testrail.yourcompany.com/index.php?/cases/view/TC-5678
def test_example():
    pass

7. 测试步骤(Step)

Step 是 Allure 中最常用的功能,将测试逻辑分解为清晰的步骤,方便定位失败原因。

7.1 方式一:with 上下文管理器(推荐)

适合在测试函数内部按步骤组织逻辑。

python 复制代码
import allure
import requests

def test_create_order():
    with allure.step("第一步:登录获取 token"):
        response = requests.post("https://api.example.com/login", json={
            "username": "test_user",
            "password": "Test@123"
        })
        token = response.json()["token"]
        assert response.status_code == 200

    with allure.step("第二步:查询商品库存"):
        response = requests.get(
            "https://api.example.com/products/SKU001",
            headers={"Authorization": f"Bearer{token}"}
        )
        assert response.json()["stock"] > 0

    with allure.step("第三步:创建订单"):
        response = requests.post(
            "https://api.example.com/orders",
            headers={"Authorization": f"Bearer{token}"},
            json={"product_id": "SKU001", "quantity": 2}
        )
        order_id = response.json()["order_id"]
        assert response.status_code == 201

    with allure.step("第四步:验证订单状态为 pending"):
        response = requests.get(
            f"https://api.example.com/orders/{order_id}",
            headers={"Authorization": f"Bearer{token}"}
        )
        assert response.json()["status"] == "pending"

7.2 方式二:@allure.step 装饰器(适合封装可复用步骤)

步骤名称中可以用 {参数名} 引用函数参数,报告中会显示实际值。

python 复制代码
import allure

@allure.step("打开登录页面")
def open_login_page(driver):
    driver.get("https://example.com/login")

@allure.step("输入用户名:{username}")
def input_username(driver, username):
    driver.find_element("id", "username").send_keys(username)

@allure.step("输入密码(已脱敏)")
def input_password(driver, password):
    # 密码不传入步骤名,避免泄露
    driver.find_element("id", "password").send_keys(password)

@allure.step("点击登录按钮")
def click_login(driver):
    driver.find_element("id", "login-btn").click()

@allure.step("验证跳转到首页")
def verify_redirect_to_home(driver):
    assert "dashboard" in driver.current_url

# 在测试用例中调用
def test_ui_login(driver):
    open_login_page(driver)
    input_username(driver, "admin")
    input_password(driver, "Admin@123")
    click_login(driver)
    verify_redirect_to_home(driver)

7.3 步骤嵌套

步骤可以无限嵌套,报告中会以树形结构展示。

python 复制代码
def test_full_purchase():
    with allure.step("一、用户登录"):
        with allure.step("打开登录页"):
            pass
        with allure.step("填写凭据"):
            pass
        with allure.step("提交并验证 token"):
            pass

    with allure.step("二、选购商品"):
        with allure.step("搜索关键词"):
            pass
        with allure.step("选择第一个商品"):
            pass
        with allure.step("加入购物车"):
            pass

    with allure.step("三、结算支付"):
        with allure.step("确认收货地址"):
            pass
        with allure.step("选择支付方式"):
            pass
        with allure.step("完成支付"):
            pass

    with allure.step("四、验证订单"):
        with allure.step("进入订单列表"):
            pass
        with allure.step("确认最新订单状态为已支付"):
            pass

7.4 步骤中添加附件

步骤内部可以直接附加信息,这些附件会挂载在该步骤下。

python 复制代码
def test_api_response():
    with allure.step("发送请求"):
        response = requests.get("https://jsonplaceholder.typicode.com/users/1")

        # 在步骤内附加请求和响应信息
        allure.attach(
            f"GET https://jsonplaceholder.typicode.com/users/1\nStatus:{response.status_code}",
            name="请求详情",
            attachment_type=allure.attachment_type.TEXT
        )
        allure.attach(
            response.text,
            name="响应体",
            attachment_type=allure.attachment_type.JSON
        )

    with allure.step("验证响应"):
        assert response.status_code == 200
        assert response.json()["id"] == 1

8. 附件(Attachment)

附件让报告更直观,可以附加截图、日志、请求响应、HTML 片段等。

8.1 allure.attach() --- 直接附加内容

python 复制代码
import allure
import json

def test_with_attachments():

    # ① 纯文本
    allure.attach(
        "这是一段测试日志\n执行步骤:登录 → 下单 → 支付",
        name="执行日志",
        attachment_type=allure.attachment_type.TEXT
    )

    # ② JSON 数据(自动格式化展示)
    data = {"user": "admin", "role": "superadmin", "status": "active"}
    allure.attach(
        json.dumps(data, indent=2, ensure_ascii=False),
        name="用户数据",
        attachment_type=allure.attachment_type.JSON
    )

    # ③ HTML 片段
    allure.attach(
        "<h2 style='color:green'>✅ 测试通过</h2><p>所有断言均已通过</p>",
        name="测试结果摘要",
        attachment_type=allure.attachment_type.HTML
    )

    # ④ XML 内容
    xml_content = "<response><code>200</code><message>OK</message></response>"
    allure.attach(
        xml_content,
        name="XML 响应",
        attachment_type=allure.attachment_type.XML
    )

    # ⑤ CSV 数据
    csv_data = "id,name,email\n1,张三,zhang@example.com\n2,李四,li@example.com"
    allure.attach(
        csv_data,
        name="测试数据",
        attachment_type=allure.attachment_type.CSV
    )

    # ⑥ 图片(PNG 字节流)
    with open("screenshot.png", "rb") as f:
        allure.attach(
            f.read(),
            name="页面截图",
            attachment_type=allure.attachment_type.PNG
        )

8.2 allure.attach.file() --- 附加本地文件

python 复制代码
def test_attach_files():

    # 附加日志文件
    allure.attach.file(
        r"C:\logs\app.log",
        name="应用日志",
        attachment_type=allure.attachment_type.TEXT
    )

    # 附加截图文件
    allure.attach.file(
        "screenshots\\login_page.png",
        name="登录页截图",
        attachment_type=allure.attachment_type.PNG
    )

    # 附加 HTML 报告片段
    allure.attach.file(
        "response.html",
        name="响应页面",
        attachment_type=allure.attachment_type.HTML
    )

8.3 支持的所有附件类型

python 复制代码
allure.attachment_type.TEXT        # 纯文本   .txt
allure.attachment_type.HTML        # HTML    .html
allure.attachment_type.XML         # XML     .xml
allure.attachment_type.JSON        # JSON    .json
allure.attachment_type.CSV         # CSV     .csv
allure.attachment_type.TSV         # TSV     .tsv
allure.attachment_type.URI_LIST    # URI     .uri
allure.attachment_type.PNG         # 图片    .png
allure.attachment_type.JPG         # 图片    .jpg
allure.attachment_type.SVG         # 矢量图  .svg
allure.attachment_type.GIF         # 动图    .gif
allure.attachment_type.BMP         # 位图    .bmp
allure.attachment_type.TIFF        # 位图    .tiff
allure.attachment_type.MP4         # 视频    .mp4
allure.attachment_type.OGG         # 视频    .ogg
allure.attachment_type.WEBM        # 视频    .webm
allure.attachment_type.PDF         # PDF     .pdf
allure.attachment_type.HAR         # 网络日志 .har

8.4 UI 测试自动截图(Selenium)

python 复制代码
import allure
import pytest
from selenium import webdriver

@pytest.fixture
def driver():
    d = webdriver.Chrome()
    yield d
    d.quit()

def test_selenium_with_screenshot(driver):
    driver.get("https://example.com")

    with allure.step("验证页面标题"):
        title = driver.title
        # 截图附加到当前步骤
        allure.attach(
            driver.get_screenshot_as_png(),
            name="当前页面截图",
            attachment_type=allure.attachment_type.PNG
        )
        assert "Example" in title

8.5 失败时自动截图(conftest.py 全局配置)

详见第 12 节 conftest.py 的完整配置。


9. 参数化测试

9.1 基本参数化

python 复制代码
import allure
import pytest

@pytest.mark.parametrize("username, password, expected_code", [
    ("admin",       "Admin@123",  200),
    ("admin",       "wrongpass",  401),
    ("nonexistent", "anypass",    401),
    ("",            "Admin@123",  400),
    ("admin",       "",           400),
])
def test_login_api(username, password, expected_code):
    """每组参数在报告中显示为独立的测试用例"""
    response = requests.post(
        "https://api.example.com/login",
        json={"username": username, "password": password}
    )
    assert response.status_code == expected_code

9.2 参数化 + 动态标题

python 复制代码
@pytest.mark.parametrize("username, password, expected_code", [
    ("admin",  "Admin@123", 200),
    ("admin",  "wrongpass", 401),
    ("",       "Admin@123", 400),
])
@allure.title("登录测试 - 用户名:{username} 密码:{password} 期望状态码:{expected_code}")
def test_login_with_title(username, password, expected_code):
    pass
    # 报告标题变为:
    # "登录测试 - 用户名:admin 密码:Admin@123 期望状态码:200"
    # "登录测试 - 用户名:admin 密码:wrongpass 期望状态码:401"

9.3 用 pytest.param 添加 ID 和标记

python 复制代码
@pytest.mark.parametrize("username, password, expected", [
    pytest.param("admin", "Admin@123", True,
                 id="正常登录",
                 marks=pytest.mark.smoke),
    pytest.param("admin", "wrongpass", False,
                 id="密码错误",
                 marks=pytest.mark.regression),
    pytest.param("", "Admin@123", False,
                 id="用户名为空",
                 marks=[pytest.mark.regression, pytest.mark.xfail]),
])
def test_login_scenarios(username, password, expected):
    result = do_login(username, password)
    assert result == expected

9.4 参数化 + 数据驱动(从文件读取)

python 复制代码
import json
import pytest
import allure

# 从 JSON 文件加载测试数据
def load_test_data(file_path):
    with open(file_path, encoding="utf-8") as f:
        return json.load(f)

# test_data.json:
# [
#   {"username": "admin", "password": "Admin@123", "expected": 200},
#   {"username": "admin", "password": "wrong",     "expected": 401}
# ]

test_cases = load_test_data("test_data\\login_data.json")

@pytest.mark.parametrize("case", test_cases, ids=[c["username"] for c in test_cases])
@allure.title("登录测试 -{case[username]}")
def test_login_from_file(case):
    response = requests.post("/api/login", json={
        "username": case["username"],
        "password": case["password"]
    })
    assert response.status_code == case["expected"]

10. 动态更新(allure.dynamic)

allure.dynamic 允许在测试运行时(而非定义时)动态修改报告中的信息,常用于根据运行条件灵活设置报告字段。

10.1 所有 dynamic 方法

python 复制代码
import allure
import pytest

def test_dynamic_example():

    # 动态标题(会覆盖 @allure.title)
    allure.dynamic.title("根据运行时环境动态设置的标题")

    # 动态描述
    allure.dynamic.description("在运行时生成的描述信息,可包含动态数据")

    # 动态 HTML 描述
    allure.dynamic.description_html("<b>动态 HTML 描述</b>")

    # 动态严重程度
    allure.dynamic.severity(allure.severity_level.CRITICAL)

    # 动态 Epic / Feature / Story
    allure.dynamic.epic("动态设置的 Epic")
    allure.dynamic.feature("动态设置的 Feature")
    allure.dynamic.story("动态设置的 Story")

    # 动态 Tag
    allure.dynamic.tag("smoke", "regression")

    # 动态 Label
    allure.dynamic.label("owner", "QA Team B")
    allure.dynamic.label("layer", "api")

    # 动态链接
    allure.dynamic.link("https://example.com", name="动态链接")
    allure.dynamic.issue("BUG-9999")
    allure.dynamic.testcase("TC-8888")

10.2 实际使用场景

场景一:根据测试环境动态设置标题

python 复制代码
import os
import allure

def test_api_health_check():
    env = os.getenv("TEST_ENV", "dev")
    url = {
        "dev":     "https://dev-api.example.com/health",
        "staging": "https://staging-api.example.com/health",
        "prod":    "https://api.example.com/health",
    }[env]

    allure.dynamic.title(f"[{env.upper()}] API 健康检查")
    allure.dynamic.description(f"检查环境:{env}\n接口地址:{url}")

    response = requests.get(url)
    assert response.status_code == 200

场景二:参数化测试中动态调整严重程度

python 复制代码
@pytest.mark.parametrize("level, username", [
    ("blocker",  "admin"),
    ("critical", "operator"),
    ("normal",   "viewer"),
])
def test_user_permission(level, username):
    # 根据参数动态设置严重程度
    severity_map = {
        "blocker":  allure.severity_level.BLOCKER,
        "critical": allure.severity_level.CRITICAL,
        "normal":   allure.severity_level.NORMAL,
    }
    allure.dynamic.severity(severity_map[level])
    allure.dynamic.title(f"用户权限测试 -{username}({level})")

    # 执行测试逻辑...

11. Fixture 与 Allure 集成

11.1 在 Fixture 中使用 Step

python 复制代码
import allure
import pytest
import requests

@pytest.fixture(scope="function")
def auth_token():
    """获取认证 Token 的 Fixture"""
    with allure.step("[Fixture] 登录获取 Token"):
        response = requests.post("https://api.example.com/login", json={
            "username": "test_user",
            "password": "Test@123"
        })
        assert response.status_code == 200
        token = response.json()["token"]
        allure.attach(
            f"Token:{token[:10]}...",
            name="Token 信息(脱敏)",
            attachment_type=allure.attachment_type.TEXT
        )
    yield token
    with allure.step("[Fixture] 清理:注销登录"):
        requests.post(
            "https://api.example.com/logout",
            headers={"Authorization": f"Bearer{token}"}
        )

def test_get_user_profile(auth_token):
    with allure.step("获取用户资料"):
        response = requests.get(
            "https://api.example.com/profile",
            headers={"Authorization": f"Bearer{auth_token}"}
        )
    with allure.step("验证响应"):
        assert response.status_code == 200
        assert "name" in response.json()

11.2 Fixture 的 Scope 与 Allure

python 复制代码
# scope="session":整个测试会话只执行一次
@pytest.fixture(scope="session")
def base_url():
    with allure.step("[Session Fixture] 读取测试环境配置"):
        url = os.getenv("BASE_URL", "https://dev-api.example.com")
        allure.attach(f"Base URL:{url}", name="环境配置", attachment_type=allure.attachment_type.TEXT)
    return url

# scope="module":每个测试模块执行一次
@pytest.fixture(scope="module")
def test_database():
    with allure.step("[Module Fixture] 初始化测试数据库"):
        db = connect_test_db()
        db.reset()
    yield db
    with allure.step("[Module Fixture] 清理测试数据库"):
        db.cleanup()
        db.close()

12. conftest.py 全局配置

conftest.py 是 pytest 的全局配置文件,结合 Allure 可以实现很多自动化的增强功能。

12.1 完整的 conftest.py 示例

python 复制代码
# tests/conftest.py
import os
import json
import allure
import pytest
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# ─────────────────────────────────────────────
# 1. 写入环境信息(在报告 Overview 中显示)
# ─────────────────────────────────────────────
def pytest_configure(config):
    """pytest 启动时写入环境信息到 allure-results"""
    import platform
    results_dir = Path("allure-results")
    results_dir.mkdir(exist_ok=True)

    env_file = results_dir / "environment.properties"
    env_file.write_text(
        f"OS=Windows{platform.version()}\n"
        f"Python={platform.python_version()}\n"
        f"pytest={pytest.__version__}\n"
        f"Environment={os.getenv('TEST_ENV', 'dev')}\n"
        f"Base.URL={os.getenv('BASE_URL', 'https://dev.example.com')}\n",
        encoding="utf-8"
    )

# ─────────────────────────────────────────────
# 2. Hook:记录测试执行结果(用于截图等后处理)
# ─────────────────────────────────────────────
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """
    此 hook 在每个测试阶段(setup/call/teardown)执行后触发,
    将结果对象存入 item,方便 fixture 中读取。
    """
    outcome = yield
    rep = outcome.get_result()
    setattr(item, f"rep_{rep.when}", rep)  # item.rep_call, item.rep_setup 等

# ─────────────────────────────────────────────
# 3. Selenium Driver Fixture(含自动截图)
# ─────────────────────────────────────────────
@pytest.fixture(scope="function")
def driver(request):
    """初始化 Chrome Driver,失败时自动截图"""
    options = Options()
    # options.add_argument("--headless")  # 无头模式(CI 环境中启用)
    options.add_argument("--start-maximized")
    options.add_argument("--disable-gpu")
    options.add_argument("--no-sandbox")

    with allure.step("[Fixture] 初始化 Chrome 浏览器"):
        d = webdriver.Chrome(options=options)

    yield d

    # teardown:失败时截图
    if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
        with allure.step("[Fixture] 失败截图"):
            allure.attach(
                d.get_screenshot_as_png(),
                name=f"失败截图 -{request.node.name}",
                attachment_type=allure.attachment_type.PNG
            )

    with allure.step("[Fixture] 关闭浏览器"):
        d.quit()

# ─────────────────────────────────────────────
# 4. API 客户端 Fixture
# ─────────────────────────────────────────────
@pytest.fixture(scope="session")
def api_client():
    """创建带认证的 requests Session"""
    import requests
    session = requests.Session()
    session.headers.update({
        "Content-Type": "application/json",
        "Accept": "application/json"
    })

    with allure.step("[Fixture] 登录获取 Token"):
        resp = session.post(
            f"{os.getenv('BASE_URL', 'https://dev.example.com')}/api/login",
            json={"username": "test_admin", "password": "TestPass@123"}
        )
        token = resp.json().get("token", "")
        session.headers.update({"Authorization": f"Bearer{token}"})

    yield session

    with allure.step("[Fixture] 注销登录"):
        session.post(f"{os.getenv('BASE_URL', '')}/api/logout")
        session.close()

# ─────────────────────────────────────────────
# 5. 自动记录 API 请求响应(对所有 API 测试生效)
# ─────────────────────────────────────────────
@pytest.fixture(autouse=False)
def log_request_response():
    """
    可以在 API 测试类上用 @pytest.mark.usefixtures("log_request_response")
    来自动附加请求响应日志。
    """
    import requests

    original_request = requests.Session.request

    def patched_request(self, method, url, **kwargs):
        response = original_request(self, method, url, **kwargs)
        log = (
            f">>> 请求 <<<\n"
            f"{method.upper()}{url}\n"
            f"Headers:{json.dumps(dict(self.headers), indent=2, ensure_ascii=False)}\n"
            f"Body:{kwargs.get('json', kwargs.get('data', ''))}\n\n"
            f">>> 响应 <<<\n"
            f"Status:{response.status_code}\n"
            f"Body:{response.text[:2000]}"  # 截取前 2000 字符
        )
        allure.attach(log, name=f"{method.upper()}{url}", attachment_type=allure.attachment_type.TEXT)
        return response

    requests.Session.request = patched_request
    yield
    requests.Session.request = original_request

13. 环境信息配置

环境信息显示在报告的 Overview 页面右侧的 "Environment" 面板中。

13.1 方式一:静态文件(environment.properties

allure-results 目录下创建 environment.properties(键值对格式):

复制代码
# allure-results/environment.properties
Browser=Chrome 120.0
Browser.Driver=ChromeDriver 120.0
OS=Windows 11 22H2
Python.Version=3.11.0
pytest.Version=7.4.0
allure-pytest.Version=2.13.2
Environment=Staging
Base.URL=https://staging.example.com
App.Version=v2.5.1
Database=PostgreSQL 15.2
Tester=QA Team A
Build.Number=42

⚠️ environment.properties 必须放在 allure-results 目录(不是 allure-report),

且必须在运行 allure generate 之前存在。

13.2 方式二:在 conftest.py 中动态生成(推荐)

python 复制代码
# conftest.py
import platform
import pytest
from pathlib import Path

def pytest_configure(config):
    """在测试开始前动态写入环境信息"""
    results_dir = Path(config.getoption("--alluredir", default="allure-results"))
    results_dir.mkdir(parents=True, exist_ok=True)

    env_props = {
        "OS":               f"Windows{platform.version()}",
        "Python.Version":   platform.python_version(),
        "pytest.Version":   pytest.__version__,
        "Environment":      os.getenv("TEST_ENV", "dev"),
        "Base.URL":         os.getenv("BASE_URL", "https://dev.example.com"),
        "App.Version":      os.getenv("APP_VERSION", "unknown"),
        "Build.Number":     os.getenv("BUILD_NUMBER", "local"),
    }

    env_file = results_dir / "environment.properties"
    content = "\n".join(f"{k}={v}" for k, v in env_props.items())
    env_file.write_text(content, encoding="utf-8")

14. 失败分类配置(Categories)

Categories 对失败和中断的测试按原因进行分组,显示在报告的 Categories 页面,方便快速定位问题类型。

14.1 创建 categories.json

在项目根目录(与 pytest.ini 同级)创建 categories.json

json 复制代码
[
  {
    "name": "🔴 产品缺陷",
    "matchedStatuses": ["failed"],
    "messageRegex": ".*AssertionError.*"
  },
  {
    "name": "🟠 环境问题",
    "matchedStatuses": ["broken"],
    "messageRegex": ".*ConnectionError.*|.*Timeout.*|.*ConnectionRefused.*"
  },
  {
    "name": "🟡 元素找不到",
    "matchedStatuses": ["broken"],
    "messageRegex": ".*NoSuchElementException.*|.*ElementNotFound.*"
  },
  {
    "name": "🟡 超时错误",
    "matchedStatuses": ["broken"],
    "messageRegex": ".*TimeoutException.*|.*ReadTimeout.*|.*ConnectTimeout.*"
  },
  {
    "name": "⚪ 跳过的测试",
    "matchedStatuses": ["skipped"]
  },
  {
    "name": "🔵 已知问题(xfail)",
    "matchedStatuses": ["failed"],
    "traceRegex": ".*xfail.*"
  }
]

14.2 将 categories.json 复制到 allure-results

conftest.py 中自动复制:

python 复制代码
import shutil
from pathlib import Path

def pytest_configure(config):
    results_dir = Path("allure-results")
    results_dir.mkdir(exist_ok=True)

    # 自动复制 categories.json 到结果目录
    categories_src = Path("categories.json")
    if categories_src.exists():
        shutil.copy(categories_src, results_dir / "categories.json")

14.3 分类规则说明

字段 类型 说明
name string 分类名称(显示在报告中)
matchedStatuses array 匹配的状态:failed / broken / skipped
messageRegex string 匹配错误信息的正则表达式
traceRegex string 匹配堆栈信息的正则表达式

15. 历史趋势

历史趋势让你追踪多次 CI/CD 运行中测试通过率、稳定性的变化,显示在报告的 Overview → Trend 中。

15.1 原理

复制代码
第 1 次运行 → 生成 allure-report/history/
第 2 次运行 → 将 history/ 复制回 allure-results/ → 生成包含趋势的新报告

15.2 Windows 脚本(bat 文件)

bash 复制代码
@echo off
REM run_with_history.bat

echo ========== 第一步:保留历史数据 ==========
if exist allure-report\history (
    echo 发现历史数据,复制到 allure-results...
    xcopy /E /I /Y allure-report\history allure-results\history
)

echo ========== 第二步:运行测试 ==========
pytest tests\ --alluredir=allure-results --clean-alluredir

echo ========== 第三步:生成报告(包含历史趋势)==========
allure generate allure-results -o allure-report --clean

echo ========== 第四步:打开报告 ==========
allure open allure-report

pause

⚠️ 注意:--clean-alluredir 会清空 allure-results,所以必须在运行 pytest 之前 先把 history 复制进去。上面的脚本顺序是正确的:先复制 history,然后再加 --clean-alluredir 运行测试(实际上可以去掉该选项,手动控制清理)。

15.3 修正版 bat 脚本(更安全)

bash 复制代码
@echo off

echo ========== 备份历史数据 ==========
if exist allure-report\history (
    if exist allure-results\history rmdir /s /q allure-results\history
    xcopy /E /I /Y allure-report\history allure-results\history
    echo 历史数据已备份
) else (
    echo 无历史数据,首次运行
)

echo ========== 运行测试(保留 history)==========
REM 不加 --clean-alluredir,手动管理
REM 先删除旧的 result/container JSON,保留 history
for %%f in (allure-results\*-result.json) do del "%%f"
for %%f in (allure-results\*-container.json) do del "%%f"

pytest tests\ --alluredir=allure-results

echo ========== 生成报告 ==========
allure generate allure-results -o allure-report --clean

echo ========== 打开报告 ==========
allure open allure-report

pause

16. 测试筛选与过滤

16.1 按 Allure 标签筛选

powershell 复制代码
# 只运行标记为 critical 和 blocker 的测试
pytest tests\ --alluredir=allure-results --allure-severities=critical,blocker

# 只运行指定 Feature 的测试
pytest tests\ --alluredir=allure-results --allure-features="用户中心"

# 只运行指定 Story 的测试
pytest tests\ --alluredir=allure-results --allure-stories="账号登录"

# 只运行指定 Epic 的测试
pytest tests\ --alluredir=allure-results --allure-epics="电商平台"

# 组合使用
pytest tests\ --alluredir=allure-results `
    --allure-features="用户中心" `
    --allure-severities=critical,blocker

16.2 按 pytest.mark 筛选

python 复制代码
# 在测试中打标记
@pytest.mark.smoke
@allure.severity(allure.severity_level.CRITICAL)
def test_login():
    pass

@pytest.mark.regression
def test_profile_update():
    pass
powershell 复制代码
# 只运行 smoke 测试
pytest tests\ -m smoke --alluredir=allure-results

# 只运行非 regression 测试
pytest tests\ -m "not regression" --alluredir=allure-results

# 组合条件
pytest tests\ -m "smoke and not slow" --alluredir=allure-results

16.3 按文件/目录/函数筛选

powershell 复制代码
# 运行指定文件
pytest tests\test_login.py --alluredir=allure-results

# 运行指定目录
pytest tests\api\ --alluredir=allure-results

# 运行指定测试函数
pytest tests\test_login.py::TestLogin::test_login_success --alluredir=allure-results

# 按关键字筛选(函数名/类名包含关键字)
pytest tests\ -k "login" --alluredir=allure-results
pytest tests\ -k "login and not logout" --alluredir=allure-results

17. 完整项目示例

下面是一个完整的 API 测试项目示例,整合了本指南中的所有核心功能。

目录结构

复制代码
api_test_project\
├── tests\
│   ├── conftest.py
│   ├── test_user_api.py
│   └── test_order_api.py
├── allure-results\       ← 自动生成
├── allure-report\        ← 自动生成
├── categories.json
├── environment.properties
├── pytest.ini
├── requirements.txt
└── run_test.bat

pytest.ini

复制代码
[pytest]
addopts=
    --alluredir=allure-results
    --clean-alluredir
testpaths= tests

categories.json

json 复制代码
[
  {
    "name": "产品缺陷",
    "matchedStatuses": ["failed"],
    "messageRegex": ".*AssertionError.*"
  },
  {
    "name": "接口异常",
    "matchedStatuses": ["broken"],
    "messageRegex": ".*ConnectionError.*|.*Timeout.*"
  },
  {
    "name": "跳过",
    "matchedStatuses": ["skipped"]
  }
]

conftest.py

python 复制代码
import os
import json
import shutil
import platform
import allure
import pytest
from pathlib import Path

def pytest_configure(config):
    results_dir = Path("allure-results")
    results_dir.mkdir(exist_ok=True)

    # 写入环境信息
    env = {
        "OS": f"Windows{platform.version()}",
        "Python": platform.python_version(),
        "Environment": os.getenv("TEST_ENV", "dev"),
        "Base.URL": os.getenv("BASE_URL", "https://jsonplaceholder.typicode.com"),
    }
    (results_dir / "environment.properties").write_text(
        "\n".join(f"{k}={v}" for k, v in env.items()), encoding="utf-8"
    )

    # 复制失败分类配置
    if Path("categories.json").exists():
        shutil.copy("categories.json", results_dir / "categories.json")

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    setattr(item, f"rep_{rep.when}", rep)

tests/test_user_api.py

python 复制代码
import allure
import pytest
import requests

BASE_URL = "https://jsonplaceholder.typicode.com"

@allure.epic("JSONPlaceholder API 测试")
@allure.feature("用户接口")
class TestUserAPI:

    @allure.story("获取用户列表")
    @allure.title("正常获取所有用户")
    @allure.severity(allure.severity_level.CRITICAL)
    @pytest.mark.smoke
    def test_get_all_users(self):
        with allure.step("发送 GET /users 请求"):
            response = requests.get(f"{BASE_URL}/users")
            allure.attach(
                response.text,
                name="响应体",
                attachment_type=allure.attachment_type.JSON
            )

        with allure.step("验证状态码为 200"):
            assert response.status_code == 200

        with allure.step("验证返回用户数量为 10"):
            users = response.json()
            assert len(users) == 10

    @allure.story("获取单个用户")
    @allure.severity(allure.severity_level.NORMAL)
    @pytest.mark.parametrize("user_id, expected_name", [
        (1, "Leanne Graham"),
        (2, "Ervin Howell"),
        (3, "Clementine Bauch"),
    ])
    @allure.title("根据 ID 获取用户 - ID:{user_id}")
    def test_get_user_by_id(self, user_id, expected_name):
        with allure.step(f"发送 GET /users/{user_id} 请求"):
            response = requests.get(f"{BASE_URL}/users/{user_id}")

        with allure.step("验证状态码为 200"):
            assert response.status_code == 200

        with allure.step(f"验证用户名为{expected_name}"):
            assert response.json()["name"] == expected_name

    @allure.story("创建用户")
    @allure.severity(allure.severity_level.CRITICAL)
    @allure.testcase("TC-001", "创建用户正向测试")
    @allure.issue("BUG-0000", "已知问题占位")
    def test_create_user(self):
        new_user = {
            "name": "测试用户",
            "username": "testuser",
            "email": "test@example.com"
        }

        with allure.step("准备请求体"):
            allure.attach(
                str(new_user),
                name="请求体",
                attachment_type=allure.attachment_type.JSON
            )

        with allure.step("发送 POST /users 请求"):
            response = requests.post(f"{BASE_URL}/users", json=new_user)

        with allure.step("验证状态码为 201"):
            allure.attach(
                response.text,
                name="响应体",
                attachment_type=allure.attachment_type.JSON
            )
            assert response.status_code == 201

        with allure.step("验证返回数据包含新增用户 ID"):
            data = response.json()
            assert "id" in data
            assert data["name"] == "测试用户"

    @allure.story("获取不存在的用户")
    @allure.severity(allure.severity_level.MINOR)
    def test_get_nonexistent_user(self):
        allure.dynamic.title("获取 ID=9999 的不存在用户,期望 404")

        with allure.step("发送 GET /users/9999 请求"):
            response = requests.get(f"{BASE_URL}/users/9999")

        with allure.step("验证状态码为 404"):
            assert response.status_code == 404

    @allure.story("删除用户")
    @allure.severity(allure.severity_level.NORMAL)
    @pytest.mark.skip(reason="删除接口暂未开放,跳过测试")
    def test_delete_user(self):
        pass

run_test.bat

bash 复制代码
@echo off
chcp 65001 > nul

echo ============================================
echo       pytest + Allure 自动化测试
echo ============================================

echo [1/3] 运行测试...
pytest tests\ -v

echo [2/3] 生成报告...
allure generate allure-results -o allure-report --clean

echo [3/3] 打开报告...
allure open allure-report

pause

18. 常见问题与排查

❓ 问题 1:报告页面空白 / 无内容

原因 :直接双击打开 index.html,浏览器 CORS 限制阻止了本地文件加载。

解决

powershell 复制代码
# 方式一:使用 allure open(推荐)
allure open allure-report

# 方式二:Python 启动本地服务
cd allure-report
python -m http.server 8080
# 然后浏览器访问 http://localhost:8080

❓ 问题 2:allure 命令无法识别

原因:Allure CLI 未安装,或 PATH 未配置。

解决

powershell 复制代码
# 检查是否安装
where allure

# 如使用 Scoop 安装
scoop install allure

# 手动安装后,确认 PATH 包含 C:\allure-x.x.x\bin
$env:Path

# 临时添加到当前会话(重启后失效)
$env:Path += ";C:\allure-2.27.0\bin"

❓ 问题 3:allure-results 目录没有生成

原因allure-pytest 未安装,或命令参数有误。

解决

powershell 复制代码
# 确认已安装
pip show allure-pytest

# 确认命令参数(注意是双横线)
pytest tests\ --alluredir=allure-results   # ✅ 正确
pytest tests\ --allure-dir=allure-results  # ❌ 旧版参数(已废弃)

❓ 问题 4:中文内容在报告中显示乱码

原因:文件编码问题。

解决

powershell 复制代码
# 方式一:在 PowerShell 中设置编码
$OutputEncoding = [Console]::OutputEncoding = [Text.Encoding]::UTF8

# 方式二:CMD 中设置 UTF-8
chcp 65001

# 方式三:Python 文件中确保使用 UTF-8 写入附件
allure.attach(
    content.encode("utf-8").decode("utf-8"),
    name="中文日志",
    attachment_type=allure.attachment_type.TEXT
)

❓ 问题 5:历史趋势不显示

原因 :生成新报告时 allure-results/history 不存在。

解决 :确保在运行 pytest 之前,将上次报告的 history 目录复制到 allure-results

powershell 复制代码
# 在 PowerShell 中
if (Test-Path "allure-report\history") {
    Copy-Item -Recurse -Force "allure-report\history" "allure-results\history"
}
pytest tests\ --alluredir=allure-results
allure generate allure-results -o allure-report --clean

❓ 问题 6:environment.properties 不显示在报告中

原因 :文件放错位置(应在 allure-results,不是 allure-report)。

解决

复制代码
allure-results\
├── environment.properties  ✅ 正确位置
└── ...

allure-report\
├── environment.properties  ❌ 错误位置(不会生效)
└── index.html

❓ 问题 7:@allure.step 装饰器参数引用不生效

原因:参数名拼写错误,或使用了关键字参数。

解决

python 复制代码
# ✅ 正确:使用位置参数,参数名与函数签名一致
@allure.step("登录用户:{username}")
def login(username, password):
    pass

# ❌ 错误:参数名拼写不对
@allure.step("登录用户:{user_name}")  # 函数参数是 username,不是 user_name
def login(username, password):
    pass

19. 速查表

所有注解汇总

python 复制代码
import allure

# ── 描述类 ──────────────────────────────────────────────
@allure.title("标题")
@allure.description("描述")
@allure.description_html("<b>HTML 描述</b>")

# ── 层级标签 ─────────────────────────────────────────────
@allure.epic("史诗")
@allure.feature("功能")
@allure.story("故事")
@allure.tag("smoke", "regression")
@allure.label("key", "value")

# ── 严重程度 ─────────────────────────────────────────────
@allure.severity(allure.severity_level.BLOCKER)
@allure.severity(allure.severity_level.CRITICAL)
@allure.severity(allure.severity_level.NORMAL)    # 默认
@allure.severity(allure.severity_level.MINOR)
@allure.severity(allure.severity_level.TRIVIAL)

# ── 链接 ─────────────────────────────────────────────────
@allure.link("url", name="名称")
@allure.issue("BUG-123")
@allure.testcase("TC-456")

# ── 步骤(代码块)────────────────────────────────────────
with allure.step("步骤描述"):
    pass

# ── 步骤(函数装饰器)────────────────────────────────────
@allure.step("步骤描述,参数:{arg}")
def some_step(arg):
    pass

# ── 附件 ─────────────────────────────────────────────────
allure.attach("内容", name="名称", attachment_type=allure.attachment_type.TEXT)
allure.attach.file("文件路径", name="名称", attachment_type=allure.attachment_type.PNG)

# ── 动态更新 ──────────────────────────────────────────────
allure.dynamic.title("标题")
allure.dynamic.description("描述")
allure.dynamic.severity(allure.severity_level.CRITICAL)
allure.dynamic.epic("Epic")
allure.dynamic.feature("Feature")
allure.dynamic.story("Story")
allure.dynamic.tag("tag1", "tag2")
allure.dynamic.label("key", "value")
allure.dynamic.link("url", name="名称")
allure.dynamic.issue("BUG-xxx")
allure.dynamic.testcase("TC-xxx")

常用命令速查

powershell 复制代码
# 运行测试
pytest tests\
pytest tests\ --alluredir=allure-results --clean-alluredir
pytest tests\ -m smoke --alluredir=allure-results
pytest tests\ --allure-severities=critical,blocker

# 生成与查看报告
allure generate allure-results -o allure-report --clean
allure open allure-report
allure serve allure-results        # 推荐开发调试用
allure serve allure-results -p 9999

# 查看 Allure 版本
allure --version

相关推荐
CDN3602 小时前
CSDN 技术分享|360CDN SDK 游戏盾集成与常见问题
运维·游戏
Trouvaille ~2 小时前
【项目篇】从零手写高并发服务器(六):EventLoop事件循环——Reactor的心脏
linux·运维·服务器·c++·高并发·epoll·reactor模式
ii_best2 小时前
安卓/ios开发辅助软件按键精灵小精灵实现简单的UI多配置管理
android·ui·ios·自动化
勇闯逆流河2 小时前
【Linux】Linux进程概念(进程优先级,进程切换详解)
linux·运维·服务器
sheepfagdng2 小时前
Python-web自动化-selenium(2)
运维·selenium·自动化
小码吃趴菜2 小时前
服务器预约系统linux小项目-第三节课
运维·服务器
zzzsde2 小时前
【Linux】进程(6):程序地址空间
linux·运维·服务器
Maverick062 小时前
Oracle PDB 创建
运维·数据库·oracle
等风来不如迎风去2 小时前
【linux】tar [选项] 归档文件名 要打包的文件/目录..
linux·运维·elasticsearch