基于 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 依赖

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:

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

第二步:安装 Allure CLI

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

# 先安装 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

第三步:验证安装

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:

[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)

# 运行所有测试,结果输出到 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 open 或 allure serve 命令启动本地服务后访问。

2.3 一键脚本(Windows .bat)

在项目根目录创建 run_test.bat:

@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 可以让标题更具业务含义。

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 — 测试描述

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

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

3.3 @allure.description_html — 富文本描述

@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 基本用法

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 类级别标注(推荐)

@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 — 自定义标签

@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 — 自定义标签键值对

# 自定义任意维度的标签
@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 用法示例

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 三种链接类型

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:

@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 上下文管理器(推荐)

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

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 装饰器(适合封装可复用步骤)

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

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 步骤嵌套

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

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 步骤中添加附件

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

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() — 直接附加内容

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() — 附加本地文件

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 支持的所有附件类型

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)

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 基本参数化

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 参数化 + 动态标题

@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 和标记

@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 参数化 + 数据驱动(从文件读取)

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 方法

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 实际使用场景

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

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

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

@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

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

# 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 示例

# 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 中动态生成(推荐)

# 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:

[
  {
    "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 中自动复制:

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 分类规则说明

字段类型说明
namestring分类名称(显示在报告中)
matchedStatusesarray匹配的状态:failed / broken / skipped
messageRegexstring匹配错误信息的正则表达式
traceRegexstring匹配堆栈信息的正则表达式

15. 历史趋势

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

15.1 原理

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

15.2 Windows 脚本(bat 文件)

@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 脚本(更安全)

@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 标签筛选

# 只运行标记为 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 筛选

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

@pytest.mark.regression
def test_profile_update():
    pass
# 只运行 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 按文件/目录/函数筛选

# 运行指定文件
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

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

conftest.py

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

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

@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 限制阻止了本地文件加载。

解决:

# 方式一:使用 allure open(推荐)
allure open allure-report

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

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

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

解决:

# 检查是否安装
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 未安装,或命令参数有误。

解决:

# 确认已安装
pip show allure-pytest

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

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

原因:文件编码问题。

解决:

# 方式一:在 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 中
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 装饰器参数引用不生效

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

解决:

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

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

19. 速查表

所有注解汇总

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")

常用命令速查

# 运行测试
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

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐