使用方式
安装
pip install pytest
测试用例识别规则(默认规则)
- 文件名: 必须以
test_开头或_test.py结尾(如test_login.py)。 - 类名: 必须以
Test开头,且不能包含__init__方法。 - 方法/函数名: 必须以
test_开头。 测试运行方式 - 命令行运行:
- 运行所有用例:
pytest - 运行指定文件:
pytest test_demo.py - 运行指定目录:
pytest tests/ - 运行指定类/方法:
pytest test_demo.py::TestClass::test_method
- 运行所有用例:
- 常用参数:
-v:详细输出模式。-s:显示标准输出(即代码中的print内容)。-x:遇到第一个失败即停止运行。--html=report.html:生成 HTML 测试报告(需安装插件)。
运行
- 在 Python 脚本中通过 pytest.main() 运行。
# test_1.py
import pytest
def test_1():
assert True == True
if __name__ == '__main__':
args = ["./test_1.py", "-v","-s"]
pytest.main(args)
- 配置文件运行
pytest.ini
# pytest.ini
[pytest]
# 添加命令行参数,相当于默认执行 pytest -v -s
addopts = -v -s
# 日志配置
log_cli = true
log_cli_level = INFO
log_cli_date_format = %Y-%m-%d-%H-%M-%S
log_cli_format = %(asctime)s-%(filename)s-%(funcName)s-%(lineno)d-%(levelname)s %(message)s
# test_1.py
import pytest
import logging
def test_1():
logging.info("执行第一个测试函数")
assert True == True
def test_2():
assert 4 == 5
logging.info("函数test_2") # 前面断言为False,这一行不输出,不执行
运行指令:
pytest ./test_1.py
跳过测试
整个文件
方法一:使用pytestmark (推荐)
import pytest
# 这里的pytestmark不可以不写,原因必须写,写在文件最前面
pytestmark = pytest.mark.skip("模块构建中。。。")
# 。。。。。。文件上下文。。。。。。
方法二:使用配置文件
# pytest.ini
[pytest]
# 忽略特定的文件或目录(可以指定多个)
addopts =
--ignore=./test_1.py
--ingnore=./test_2.py
# 有not 是跳过测试函数,无not指定运行函数
-k "not _one"
# 测试函数会被跳过
def test_1_one():
assert True == True
跳过某个函数
- 无条件跳过测试函数
@pytest.mark.skip(reason="todo ")
def test_2():
assert True == 1
- 满足条件,跳过测试
@pytest.mark.skipif(sys.platform == "win32", reason="此平台不运行")
def test_unix_specific():
"""在非 Windows 系统上运行,在 Windows 上跳过"""
logging.info("sys.platform == win32")
assert True
@pytest.mark.skipif(sys.platform == "win64", reason="此平台不运行")
def test_unix_specific2():
"""在非 Windows 系统上运行,在 Windows 上跳过"""
logging.info("sys.platform == win64")
assert True
- 动态跳过测试函数
def test_dynamic_skip():
if True:
logging.info("动态跳过测试前代码")
pytest.skip("不满足条件,跳过")
logging.info("动态跳过后测试代码") # 该行代码不执行
fixture
fixture 是一种用于设置测试环境的机制。它们可以用来执行一些前置或后置操作(例如:准备数据、启动服务、清理状态等),并且可以在多个测试之间共享。作用域是决定其执行顺序的首要因素。pytest遵循作用域越小,越先执行;作用域越大,越后执行的原则
作用域
函数级、类级、模块级:它决定了 fixture 函数在何时被创建和销毁。通过 scope 参数设置
| 作用域 | 说明 | 创建/销毁时机 | 典型使用场景 |
|---|---|---|---|
function (默认) | 每个测试函数都运行一次 | 测试函数执行前创建,执行后销毁 | 需要为每个测试用例提供独立、干净的测试环境时。如:每个测试用例都需要独立的数据库事务、临时文件等。 |
class | 每个测试类运行一次 | 类中第一个测试方法执行前创建,类中最后一个测试方法执行后销毁 | 当一个类中的所有测试方法可以共享同一个资源时,能提高效率。如:一个类中的测试都使用同一个配置对象。 |
module | 每个测试文件(模块)运行一次 | 模块中第一个测试函数执行前创建,模块中最后一个测试函数执行后销毁 | 对于整个模块的测试都共享的资源。如:一个模块的测试都连接到同一个测试数据库实例。 |
session | 整个测试会话只运行一次 | 整个测试会话开始时创建,所有测试结束后销毁 | 对于非常昂贵或全局唯一的资源。如:启动一个外部服务、建立到全局数据库的连接、读取全局配置文件等。 |
import logging
import pytest
scope_flage = {
"func": 0,
"class": 0,
"moudle": 0
}
def scope_init(scope="func"):
global scope_flage
if scope == "func":
scope_flage["func"] += 5
logging.info(scope_flage)
elif scope == "class":
scope_flage["class"] += 5
logging.info(scope_flage)
elif scope == "moudle":
scope_flage["moudle"] += 5
logging.info(scope_flage)
logging.info("数据初始化成功")
return scope_flage
def close_init():
global scope_flage
scope_flage = {
"func": 0,
"class": 0,
"moudle": 0
}
logging.info(scope_flage)
logging.info("回收数据成功")
return True
@pytest.fixture
def db():
"""创建并返回一个数据库连接(函数级别)"""
logging.info("\n🔧 [Fixture] 初始化数据资源")
yield scope_init("func") # 测试执行点
logging.info("🧹 [Fixture] 回收数据资源")
close_init()
def test_operation_func1(db):
db["func"] += 13
logging.info(db)
assert True == True
def test_operation_func2(db):
logging.info(db)
assert True == True
@pytest.fixture(scope="module")
def module_db():
"""模块级别 Fixture(整个测试文件只初始化一次)"""
logging.info("\n🌍 [Module Fixture] 初始化数据资源(模块级)")
yield scope_init("moudle")
logging.info("\n🌍 [Module Fixture] 关闭数据资源(模块级)")
close_init()
def test_operation_moudle3(module_db):
module_db["moudle"] += 13
logging.info(module_db)
assert True == True
def test_operation_moudle4(module_db):
logging.info(module_db)
assert True == True
@pytest.fixture(scope="class")
def class_db():
"""类级别 Fixture(整个测试类只初始化一次)"""
logging.info("\n👥 [Class Fixture] 初始化数据资源(类级)")
yield scope_init("class")
logging.info("\n👥 [Class Fixture] 关闭数据资源(类级)")
close_init()
class TestClassScope:
def test_class1_scope(self, class_db):
"""验证类级 Fixture 作用域"""
class_db["class"] += 13
logging.info(class_db)
assert 1 == 1
def test_class2_scope(self, class_db):
"""验证类级 Fixture 作用域(同一个类)"""
logging.info(class_db)
assert 1 == 1
pytest.mark.usefixtures
如果你不想在每个测试函数中都列出所有的 fixtures,或者你需要为多个测试函数应用同一个 fixture,或者一个测试函数使用多个fixture,可以使用 pytest.mark.usefixtures 来标记这些测试函数或整个测试类
@pytest.fixture
def start1_fix():
logging.info("加载资源1")
yield
logging.info("注销资源1")
@pytest.fixture
def start2_fix():
logging.info("加载资源2")
yield
logging.info("注销资源2")
@pytest.fixture
def start3_fix():
logging.info("加载资源3")
yield
logging.info("注销资源3")
@pytest.fixture(autouse=True) # 所有的测试函数都会调用该fixture
def all_used_fixture():
logging.info("这个夹具会自动应用到所有测试用例中。")
@pytest.mark.usefixtures("start1_fix")
def test_1():
assert True == True
@pytest.mark.usefixtures("start1_fix", "start2_fix", "start3_fix")
def test_2():
assert True == True
def test_3():
assert True == True
conftest.py:共享 fixture 的利器
你不必在每个测试文件中都重复定义相同的 fixture。pytest 允许你在一个名为 conftest.py 的特殊文件中定义 fixture,这个文件中的 fixture 对其所在目录及其子目录下的所有测试文件都是可见且可用的
tests/
├── conftest.py # 定义了 common_fixture
├── test_module_a.py
└── sub_directory/
├── conftest.py # 可以覆盖或扩展父级的 fixture
└── test_module_b.py
conftest.py是一个无需导入的神奇文件,pytest 会自动发现并加载它。 它通常用于存放项目级或目录级的共享 fixture,比如数据库连接、配置加载等。 不同目录层级可以有多个 conftest.py,子目录的 conftest.py 中的 fixture 可以覆盖父目录中的同名 fixture。
fixture相互依赖
@pytest.fixture
def before_fix():
logging.info("加载前置资源1")
yield [0,0]
logging.info("注销前置资源1")
@pytest.fixture
def end_fix(before_fix): # 依赖 before_fix if before_fix[0] == True:
logging.info("加载前置资源2-1")
yield [1]
logging.info("注销前置资源2-1")
elif before_fix[1] == False:
logging.info("加载前置资源2-2")
yield [0]
logging.info("注销前置资源2-2")
else:
logging.info("加载前置资源2-3")
yield [1,1,0,0]
logging.info("注销前置资源2-3")
def test_user_creation(end_fix):
assert end_fix == [0]
fixture参数传递
@pytest.fixture
def data_engine(request):
logging.info(f"⚙️ [Param Fixture] 使用 {request.param} 引擎")
yield {
"size": 10,
"works": 20
}[request.param]
logging.info("关闭数据引擎")
@pytest.mark.parametrize("data_engine", ["works", "size"], indirect=True)
def test_parametrized_db(data_engine):
assert data_engine in [10,20]
名字的重用: Fixture 的名字和测试函数参数的名字是故意设计成一样的,这是一种“魔法”。这个魔法就是依赖注入。
xfail失败通过标记
import pytest
@pytest.mark.xfail(reason="业务1未完成,必定失败")
def test_1():
assert True == True
@pytest.mark.xfail(reason="业务2未完成,必定失败")
def test_2():
"""预期失败:应该失败,但不计入测试失败)"""
assert True == False
def divide(a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
@pytest.mark.xfail(raises=ValueError, reason="已知缺陷:除以零会报错")
def test_divide_by_zero():
# 这个调用会触发 ValueError divide(10, 0)
def test_divide_success():
# 这是一个正常的测试,不会失败
assert divide(10, 2) == 5
自定义标记
在测试文件中使用 pytest.mark.xxx 即可自动注册标记 ,不需要额外配置(pytest 会自动识别)
# =================== 冒烟测试(带自定义标记) ===================
@pytest.mark.smoke
def test_login_success_smoke():
"""冒烟测试:成功登录"""
logging.info("✅ 正在运行冒烟测试:登录成功")
assert True
@pytest.mark.smoke
def test_payment_process_smoke():
"""冒烟测试:支付流程"""
logging.info("✅ 正在运行冒烟测试:支付流程")
assert True
# =================== 长时间测试(带其他标记) ===================
@pytest.mark.long
def test_full_user_flow_long():
"""长时间测试:完整用户流程"""
logging.info("⏳ 正在运行长时间测试:完整用户流程")
assert True
# =================== 重要功能测试 ===================
@pytest.mark.feature("user_management")
def test_user_creation_feature(request):
"""功能测试:创建用户"""
logging.info("⚙️ 正在运行功能测试:创建用户")
# 获取生成器
markers = request.node.iter_markers(name="feature")
# 获取第一个标记对象,如果没有则返回 None marker = next(markers, None)
# 提取参数
feature_name = marker.args[0] if marker else "未知"
logging.info(f"当前正在测试功能模块: {feature_name}")
assert True
if __name__ == "__main__":
pytest.main([
"test_4.py", # 测试文件
"-m", # 指定标记
"smoke", # 标记名称
"-v", # 详细输出
])
配置文件pytest.ini【没有配置会有警告】
# pytest.ini
[pytest]
markers =
smoke: 标记为冒烟测试
long: 标记为耗时较长的测试
feature: 标记为某个功能模块的测试
test_user_creation_feature的测试结果:
test_4.py::test_user_creation_feature
-------------------- live log call -----------------------
2026-03-29-00-35-06-test_4.py-test_user_creation_feature-70-INFO ⚙️ 正在运行功能测试:创建用户
2026-03-29-00-35-06-test_4.py-test_user_creation_feature-78-INFO 当前正在测试功能模块: user_management
PASSED
pytest.mark.parametrize传递参数
@pytest.mark.parametrize 核心作用是:让同一个测试函数,使用不同的参数组合运行多次。
基本语法格式:
@pytest.mark.parametrize("参数名1,参数名2", [(值1_1, 值1_2), (值2_1, 值2_2)])
def test_func(参数名1, 参数名2):
...
基础用法(单参数)
import pytest
def add(var:int):
return var
# 定义参数:第一个字符串是"参数名",第二个列表是"参数值列表"
@pytest.mark.parametrize("case",[1,2,3,4])
def test_add(case):
print(f"测试方法add({case})开始执行")
assert add(case) == case
多参数组合(列表对应)
你可以传入多个参数。注意:列表中的元组个数决定了运行次数,元组内的值对应参数名。
import pytest
@pytest.mark.parametrize("username, password, is_valid", [
("admin", "123456", True), # 用例 1
("user", "wrong", False), # 用例 2
("", "123456", False), # 用例 3: 空用户名
("admin", "", False), # 用例 4: 空密码
])
def test_login_logic(username, password, is_valid):
"""模拟登录验证逻辑"""
# 模拟业务逻辑
if username == "admin" and password == "123456":
actual_valid = True
else:
actual_valid = False
assert actual_valid == is_valid, f"用户 {username} 的验证结果错误"
💡 注意:这里不是笛卡尔积(即不会把每个用户名和每个密码都组合一遍),而是一一对应。列表里有几个元组,就运行几次。
自定义测试 ID
如果参数很长或包含特殊字符,生成的测试 ID 会很乱。可以使用 ids 参数自定义名称。
import pytest
@pytest.mark.parametrize("price, discount, expected", [
(100, 0.9, 90),
(200, 0.8, 160),
(50, 1.0, 50),
], ids=["九折优惠", "八折优惠", "无折扣"])
def test_calc_price(price, discount, expected):
final_price = price * discount
assert final_price == expected
运行效果:
test_calc_price[九折优惠]
test_calc_price[八折优惠]
test_calc_price[无折扣]
(在 HTML 报告或 Allure 中非常有用)
嵌套 Parametrize (笛卡尔积)
如果你想让两组参数完全组合(例如:2 种浏览器 × 3 种分辨率 = 6 次运行),可以堆叠两个 parametrize 装饰器。
import pytest
@pytest.mark.parametrize("browser", ["Chrome", "Firefox"])
@pytest.mark.parametrize("resolution", ["1920x1080", "1366x768", "375x667"])
def test_ui_display(browser, resolution):
"""
这将运行 2 * 3 = 6 次:
Chrome + 1920x1080
Chrome + 1366x768
Chrome + 375x667
Firefox + 1920x1080
...
"""
print(f"正在 {browser} 下测试分辨率 {resolution}")
assert True
注意:装饰器是从下往上执行的,但效果是所有的组合都会覆盖。
indirect=True
class Pizza:
def __init__(self, flavor):
self.flavor = flavor
self.is_baked = False
def bake(self):
"""烤披萨"""
self.is_baked = True
return f"🍕 热腾腾的 {self.flavor} 披萨出炉了!"
def eat(self):
"""吃披萨"""
if not self.is_baked:
return "❌ 还没烤熟,不能吃!"
return f"😋 真好吃,这是 {self.flavor} 味的!"
@pytest.fixture
def chef(request):
# 1. 获取 parametrize 传进来的口味名字 (比如 "榴莲")
flavor_name = request.param
print(f"\n👨🍳 厨师收到订单:要做一份 [{flavor_name}] 披萨...")
# 2. 厨师开始制作 (实例化对象)
my_pizza = Pizza(flavor_name)
# 3. 厨师把披萨烤好 (执行一些初始化逻辑)
my_pizza.bake()
# 4. 把做好的【披萨对象】端给测试函数
return my_pizza
# 注意这里:传进去的是字符串 ["榴莲", "牛肉"]
# 但因为加了 indirect=True,它们会先传给 chef 处理
@pytest.mark.parametrize(
"chef", # 这里的名字必须和上面的 fixture 名字一模一样
["榴莲", "牛肉"], # 这是原始数据(口味名字)
indirect=True # <--- 魔法开关:告诉 pytest "别直接传字符串,先让 chef 处理一下"
)
def test_eat_pizza(chef):
# 🔴 如果没有 indirect=True:
# chef 变量就会是字符串 "榴莲"。
# 下面这行代码会报错:AttributeError: 'str' object has no attribute 'eat'
# ✅ 有了 indirect=True:
# chef 变量已经是 <Pizza object> 了!
print(f"🍽️ 准备开吃:{chef.flavor}")
# 我们可以直接调用对象的方法
result = chef.eat()
# 断言
assert "真好吃" in result
assert chef.is_baked is True
indirect=True 是 pytest 参数化测试中最关键的参数之一。它的核心作用是:告诉 pytest:“不要把参数列表里的值直接传给测试函数,而是先把这些值传给同名的 fixture,让 fixture 处理完后,再把 fixture 的返回值传给测试函数。”
简单来说:它把“数据”变成了“fixture 的输入参数”。
通常用于以下场景:
- 复杂的对象初始化: 测试需要的不是简单的字符串或数字,而是一个配置好的对象(如数据库连接、浏览器实例、API 客户端)。你希望参数只是配置项(如 URL、用户名),而对象的创建逻辑封装在 fixture 里。
- 复用 Setup/Teardown 逻辑:
如果每个参数都需要执行相同的“清理工作”(比如测试完删除数据),你可以把清理逻辑写在 fixture 的
yield之后。无论参数怎么变,清理逻辑只写一次。 - 动态跳过测试:
可以在 fixture 内部根据
request.param的值决定是否pytest.skip(),从而灵活控制哪些参数组合需要运行。 ⚠️ 常见错误 如果忘了加indirect=True,但 fixture 里又用了request.param,或者测试函数里试图调用对象方法,通常会报这两类错: Fixture "xxx" called directly(如果你试图在参数化里直接调 fixture)AttributeError: 'str' object has no attribute '...'(因为传进来的是原始数据,不是对象) 这句话的意思是:“请分别用 'mysql' 和 'postgres' 这两个字符串去初始化db_engine这个 fixture,然后把初始化好的Database对象传给我的测试函数。”
allure-pytest库
安装:
pip install allure-pytest
下载allure
- 下载地址[Releases · allure-framework/allure2 · GitHub](Releases · allure-framework/allure2 · GitHub)
- 配置环境:.\allure-2.38.0\bin
- 验证:allure -version 使用:
pytest [测试文件] --alluredir=allure-results --clean-alluredir
allure serve ./allure-results
标记装饰器:
@allure.epic(),用于描述被测软件系统@allure.feature(),用于描述被测软件的某个功能模块@allure.story(),用于描述功能模块下的功能点或功能场景,也即测试需求@allure.title(),用于定义测试用例标题@allure.description(),用于测试用例的说明描述@allure.severity(),标记测试用例级别,由高到低分为 blocker、critical、normal、minor、trivial 五级@allure.step(),标记通用函数使之成为测试步骤,测试方法/测试函数中调用此通用函数的地方会向报告中输出步骤描述
import allure
import pytest
import os
def get_token():
return True
@allure.epic("token的epic")
@allure.feature("token的feature")
@allure.step("token的step")
@allure.title("token的title")
@allure.story("token的story")
@allure.description("token的description")
@allure.severity("token的severity")
def test_login():
assert get_token() == True
@allure.epic("token2的epic")
@allure.feature("token2的feature")
@allure.step("token2的step")
@allure.title("token2的title")
@allure.story("token2的story")
@allure.description("token2的description")
@allure.severity("token2的severity")
def test_login2():
assert get_token() == True
@allure.epic("token3的epic")
@allure.feature("token3的feature")
@allure.step("token3的step")
@allure.title("token3的title")
@allure.story("token3的story")
@allure.description("token3的description")
@allure.severity("token3的severity")
def test_login3():
assert get_token() == False
if __name__=="__main__":
os.system(r"pytest -vs ./test_5.py --alluredir=./allure-results --clean-alluredir")
# os.system(r"allure generate ./allure-results -o ./report-allure")
# os.system(r"allure serve ./allure-results")
import pytest
import allure
import time
import json
import random
from datetime import datetime
# ==============================================================================
# 1. 全局配置与 Hook (用于自动捕获失败现场)
# ==============================================================================
@allure.feature("🛍️ 电商核心交易系统")
@allure.story("订单处理流程")
class TestOrderSystem:
"""
演示类:模拟一个复杂的电商订单处理系统。
包含:参数化、动态步骤、附件上传、链接跳转、严重程度动态调整。
"""
# 模拟测试数据:不同场景的订单
test_data = [
{"case_id": "ORD-001", "user": "VIP_User", "amount": 999.00, "status": "success", "desc": "VIP用户大额订单"},
{"case_id": "ORD-002", "user": "Normal_User", "amount": 50.00, "status": "success", "desc": "普通用户小额订单"},
{"case_id": "ORD-003", "user": "Black_List", "amount": 10000.00, "status": "fail",
"desc": "黑名单用户拦截测试"},
{"case_id": "ORD-004", "user": "Timeout_User", "amount": 200.00, "status": "timeout",
"desc": "支付超时异常测试"},
]
@pytest.mark.parametrize("data", test_data, ids=lambda x: x["case_id"])
@allure.title("测试用例:{data[desc]}")
def test_process_order(self, data):
"""
核心测试逻辑:处理订单
演示点:
1. 动态设置严重程度 (Severity)
2. 动态添加参数 (Parameters)
3. 多步骤嵌套 (Steps)
4. 附加文本/JSON日志 (Attach)
5. 模拟异常与截图
"""
# --- 动态元数据设置 --- # 根据数据动态设置严重程度
if data["status"] == "fail":
allure.dynamic.severity(allure.severity_level.BLOCKER)
allure.dynamic.tag("风险拦截", "安全审计")
elif data["status"] == "timeout":
allure.dynamic.severity(allure.severity_level.CRITICAL)
allure.dynamic.tag("性能问题")
else:
allure.dynamic.severity(allure.severity_level.NORMAL)
allure.dynamic.tag("常规交易")
# 添加动态链接 (关联 Jira 或需求文档)
allure.dynamic.link(f"https://jira.example.com/browse/{data['case_id']}", name="需求追踪")
allure.dynamic.issue("https://github.com/myrepo/issues/123", name="已知问题")
# 添加动态参数 (在报告中清晰展示输入数据)
allure.dynamic.parameter("订单ID", data["case_id"])
allure.dynamic.parameter("用户类型", data["user"])
allure.dynamic.parameter("交易金额", f"${data['amount']}")
allure.dynamic.parameter("预期结果", data["status"])
# --- 执行步骤 1: 验证用户资格 ---
with allure.step("🔍 步骤 1: 验证用户资格与风控检查"):
self._log_action(f"正在查询用户 {data['user']} 的状态...")
time.sleep(0.5) # 模拟耗时
user_info = {"id": 1001, "name": data["user"], "level": "VIP" if "VIP" in data["user"] else "Normal"}
# 附加 JSON 格式的中间数据
allure.attach(
json.dumps(user_info, indent=2, ensure_ascii=False),
name="用户信息快照",
attachment_type=allure.attachment_type.JSON
)
if data["user"] == "Black_List":
# 模拟业务逻辑失败
self._log_action("❌ 检测到黑名单用户,拦截交易!")
# 附加纯文本日志
allure.attach("风控规则 ID: RULE-9527\n拦截原因:用户处于黑名单", name="风控拦截日志",
attachment_type=allure.attachment_type.TEXT)
assert False, "交易被风控系统拦截"
# --- 执行步骤 2: 扣减库存 ---
with allure.step("📦 步骤 2: 锁定并扣减库存"):
self._log_action(f"尝试锁定商品,金额:{data['amount']}")
time.sleep(0.3)
# 模拟随机生成的库存流水号
stock_flow_id = f"STK-{random.randint(10000, 99999)}"
allure.attach(stock_flow_id, name="库存流水号", attachment_type=allure.attachment_type.TEXT)
# --- 执行步骤 3: 调用支付网关 ---
with allure.step("💳 步骤 3: 调用第三方支付网关"):
self._log_action("正在连接支付网关...")
time.sleep(0.5)
if data["status"] == "timeout":
# 模拟超时异常
error_log = "ConnectionTimeout: Gateway did not respond within 3000ms"
allure.attach(error_log, name="网关错误日志", attachment_type=allure.attachment_type.TEXT)
# 【高级技巧】模拟失败时自动附加"截图"
# 在实际 UI 自动化中,这里会是 driver.get_screenshot_as_png() # 这里我们生成一个假的 PNG 头或者文本模拟截图内容,让报告看起来有截图
fake_screenshot_content = f"SIMULATED SCREENSHOT AT {datetime.now()}\nError: Payment Timeout\nUser: {data['user']}"
allure.attach(
fake_screenshot_content,
name="失败现场截图 (模拟)",
attachment_type=allure.attachment_type.PNG # 即使内容是文本,设为PNG也会在报告中显示为图片块(取决于查看器),或者用 TEXT 更稳妥
)
# 为了演示效果,这里用 TEXT 类型确保能看到内容,实际项目中请用真正的 bytes raise AssertionError("支付网关响应超时")
# --- 执行步骤 4: 生成订单 ---
with allure.step("🧾 步骤 4: 生成最终订单并发送通知"):
order_result = {"order_no": f"NO-{time.time()}", "status": "PAID"}
allure.attach(json.dumps(order_result), name="最终订单详情",attachment_type=allure.attachment_type.JSON) self._log_action("✅ 订单创建成功,通知已发送")
# 最终断言
assert data["status"] == "success", f"期望状态为 success,但实际为 {data['status']}"
def _log_action(self, message: str):
"""辅助方法:打印日志并同时写入 Allure 步骤详情(可选)"""
print(f"[LOG] {message}")
# 如果需要将每个小动作都作为子步骤,可以使用 nested step,但通常 print 足够
# 这里仅做演示,实际 log 会显示在 'Output' 标签页
# ==============================================================================
# 2. 另一个 Feature 演示:数据驱动与动态描述
# ==============================================================================
@allure.feature("📊 数据分析报表模块")
@allure.story("报表生成引擎")
class TestReportEngine:
@pytest.mark.parametrize("report_type, expected_time", [
("日报", 1.0),
("周报", 3.5),
("月报", 10.0),
])
@allure.description("""
这是一个长描述的测试用例。
它用于验证不同类型的报表生成时间是否在允许范围内。
**前置条件**:
1. 数据库连接正常
2. 数据仓库已同步
**预期行为**:
生成时间不应超过阈值。
""")
def test_generate_report_performance(self, report_type, expected_time):
"""性能测试演示"""
allure.dynamic.title(f"性能测试:{report_type}生成耗时验证")
allure.dynamic.label("owner", "DataTeam")
allure.dynamic.label("component", "ETL-Pipeline")
with allure.step(f"开始生成 {report_type}"):
start = time.time()
# 模拟处理耗时 (稍微随机一点,让它有时通过有时失败,方便演示)
# 为了演示稳定性,这里固定模拟一个接近阈值的值
actual_time = expected_time * 0.9
time.sleep(actual_time)
duration = time.time() - start
with allure.step("验证耗时"):
allure.dynamic.parameter("预期最大耗时", f"{expected_time}s")
allure.dynamic.parameter("实际耗时", f"{duration:.2f}s")
# 附加一个大的文本块模拟 SQL 执行计划
sql_plan = f"""
EXPLAIN ANALYZE SELECT * FROM huge_table WHERE type = '{report_type}'
-> Seq Scan on huge_table (cost=0.00..100.00 rows=1000) Filter: (type = '{report_type}')
Actual Time: {duration * 1000:.2f} ms """ allure.attach(sql_plan, name="SQL 执行计划", attachment_type=allure.attachment_type.TEXT)
assert duration <= expected_time, f"{report_type} 生成超时!耗时 {duration:.2f}s > {expected_time}s"