在 Python 自动化测试领域,测试框架的选择直接影响测试效率与代码可维护性。而 pytest 框架 凭借其灵活的语法、强大的插件生态和丰富的功能,成为了开发者和测试工程师的首选工具。它不仅解决了 Python 内置 unittest 框架语法繁琐、扩展性弱的痛点,还支持单元测试、集成测试、接口测试等多种场景,让 “编写测试用例” 这件事变得高效且优雅。
本文将从基础到进阶,带大家全面掌握 pytest 框架的使用 —— 从环境安装到核心语法,从参数化、夹具到实战案例,再到插件扩展技巧,帮你快速上手并搭建企业级测试体系。
一、为什么选择 pytest 框架?—— 它的核心优势
在学习使用前,我们先明确一个问题:Python 自带 unittest 框架,为什么还要用 pytest?答案藏在它的四大核心优势里:
语法极简,编写效率高
unittest 需要继承 TestCase 类、使用固定的断言方法(如 self.assertEqual()),而 pytest 无需继承类,直接用函数定义测试用例,断言可使用 Python 原生语法(如 assert a == b),代码量减少 30% 以上。例如,一个简单的测试用例,pytest 只需 3 行代码,而 unittest 至少需要 5 行。
功能强大,覆盖所有测试场景
支持参数化测试(批量生成用例)、测试夹具(前置 / 后置操作)、跳过用例、标记用例、失败重试等高级功能,无需额外封装工具类。同时,兼容 unittest 和 nose 框架的测试用例,迁移成本极低。
插件生态丰富,扩展性强
拥有超过 800 个官方及第三方插件,可轻松实现测试报告生成(pytest-html)、接口测试(pytest-requests)、异步代码测试(pytest-asyncio)、与 CI/CD 工具集成(如 Jenkins)等需求,插件安装与使用简单,一行命令即可完成配置。
输出清晰,调试效率高
测试执行结果会以彩色文本形式展示,清晰标注用例通过 / 失败状态;若用例失败,会详细输出断言对比信息(如预期值与实际值的差异)、错误堆栈跟踪,帮助快速定位问题根源。
二、环境准备:3 分钟完成安装与验证
pytest 是第三方库,需通过包管理工具安装,支持 Windows/macOS/Linux 全平台,且兼容 Python 3.7 及以上版本。
1. 安装方式(推荐 pip)
打开终端 / 命令提示符,输入以下命令,等待安装完成:
pip install pytest
若需要安装指定版本(如适配旧项目),可指定版本号:
pip install pytest==7.4.3 # 安装 7.4.3 版本
2. 验证安装是否成功
安装完成后,在终端输入以下命令,查看 pytest 版本:
pytest --version
若输出类似 pytest 7.4.3 的信息,说明安装成功。
3. 第一个 pytest 测试用例
创建一个名为 test_demo.py 的文件,写入以下代码:
# test_demo.py
def add(a, b):
"""待测试的加法函数"""
return a + b
def test_add_positive():
"""测试正数相加"""
result = add(2, 3)
assert result == 5 # 原生断言,简洁直观
def test_add_negative():
"""测试负数相加"""
result = add(-2, -3)
assert result == -5
def test_add_zero():
"""测试与零相加"""
result = add(0, 5)
assert result == 5
在终端进入该文件所在目录,执行以下命令运行测试:
pytest test_demo.py -v
-v:启用详细输出模式,展示每个用例的执行结果。
执行后,终端会输出彩色结果:
collected 3 items
test_demo.py::test_add_positive PASSED
test_demo.py::test_add_negative PASSED
test_demo.py::test_add_zero PASSED
3 个用例全部通过,说明 pytest 已能正常工作。
三、核心语法:掌握 90% 场景的用法
pytest 的核心是围绕 测试用例编写 和 测试执行控制 设计的,下面我们逐一讲解高频用法。
1. 测试用例的命名规范
pytest 会自动识别符合以下规则的文件和函数 / 类,无需手动指定用例:
测试文件:文件名以 test_ 开头(如 test_login.py)或以 test.py 结尾(如 logintest.py)。
测试函数 / 方法:函数名以 test_ 开头(如 test_login_success)。
测试类:类名以 Test 开头(如 TestLogin),且类中不含 init 方法,类内的测试方法以 test_ 开头。
示例:测试类的使用
# test_user.py
class TestUser:
"""用户相关测试类"""
def test_user_create(self):
"""测试创建用户"""
user = {"name": "张三", "age": 25}
assert user["name"] == "张三"
def test_user_update(self):
"""测试更新用户"""
user = {"name": "张三", "age": 25}
user["age"] = 26
assert user["age"] == 26
执行命令 pytest test_user.py -v,pytest 会自动识别并执行类中的两个测试方法。
2. 断言语法:简洁的原生断言
pytest 支持 Python 所有原生断言语法,无需记忆特殊方法,常用断言场景如下:
重点:异常断言示例
当需要测试函数在特定场景下是否抛出预期异常时,可使用 pytest.raises():
# test_exception.py
import pytest
def divide(a, b):
"""待测试的除法函数"""
if b == 0:
raise ZeroDivisionError("除数不能为零")
return a / b
def test_divide_by_zero():
"""测试除数为零的异常场景"""
# 断言调用 divide(5, 0) 时会抛出 ZeroDivisionError
with pytest.raises(ZeroDivisionError) as exc_info:
divide(5, 0)
# 可选:验证异常信息是否符合预期
assert "除数不能为零" in str(exc_info.value)
3. 参数化测试:批量生成用例
在测试同一功能的不同输入场景时(如登录的正确密码、错误密码、空密码),无需编写多个重复的测试函数,可使用 @pytest.mark.parametrize 装饰器批量生成用例,大幅减少代码冗余。
示例:登录功能参数化测试
# test_login_param.py
import pytest
def login(username, password):
"""模拟登录函数:用户名和密码都为 "admin" 时返回成功"""
if username == "admin" and password == "admin":
return {"status": "success", "message": "登录成功"}
else:
return {"status": "fail", "message": "用户名或密码错误"}
# 参数化装饰器:第一个参数是用例参数名(元组),第二个参数是参数值列表(每个元素是一个用例的参数)
@pytest.mark.parametrize("username, password, expected_status, expected_msg", [
# 用例 1:正确用户名和密码
("admin", "admin", "success", "登录成功"),
# 用例 2:正确用户名,错误密码
("admin", "123456", "fail", "用户名或密码错误"),
# 用例 3:错误用户名,正确密码
("test", "admin", "fail", "用户名或密码错误"),
# 用例 4:空用户名
("", "admin", "fail", "用户名或密码错误"),
# 用例 5:空密码
("admin", "", "fail", "用户名或密码错误"),
])
def test_login_parametrize(username, password, expected_status, expected_msg):
"""登录功能参数化测试"""
result = login(username, password)
assert result["status"] == expected_status
assert result["message"] == expected_msg
执行命令 pytest test_login_param.py -v,会看到 5 个用例被自动生成并执行:
collected 5 items
test_login_param.py::test_login_parametrize[admin-admin-success-登录成功] PASSED
test_login_param.py::test_login_parametrize[admin-123456-fail-用户名或密码错误] PASSED
test_login_param.py::test_login_parametrize[test-admin-fail-用户名或密码错误] PASSED
test_login_param.py::test_login_parametrize[-admin-fail-用户名或密码错误] PASSED
test_login_param.py::test_login_parametrize[admin--fail-用户名或密码错误] PASSED
4. 测试夹具(Fixture):灵活的前置 / 后置操作
在测试中,常需要重复执行一些操作(如初始化数据库连接、创建测试用户、清理测试数据),pytest 的 Fixture 功能可替代传统的 setup()/teardown() 方法,支持灵活的作用域控制和依赖注入,是 pytest 的核心亮点之一。
(1)基础 Fixture:定义前置操作
创建一个名为 conftest.py 的文件(pytest 会自动识别该文件中的 Fixture,无需导入),写入以下代码:
# conftest.py
import pytest
@pytest.fixture
def init_database():
"""Fixture:初始化数据库连接(前置操作)"""
print("\n=== 初始化数据库连接 ===")
db_conn = "模拟数据库连接对象" # 实际场景中可替换为真实的数据库连接
yield db_conn # yield 之前是前置操作,之后是后置操作
print("\n=== 关闭数据库连接 ===") # 测试用例执行完后执行后置操作
在测试用例中,直接将 Fixture 名作为参数传入,即可使用 Fixture 的返回值:
# test_db.py
def test_query_user(init_database):
"""测试查询用户:依赖 init_database Fixture"""
# init_database 是 Fixture 的返回值(数据库连接对象)
print(f"使用数据库连接:{init_database}")
# 模拟查询操作
user = {"id": 1, "name": "张三"}
assert user["id"] == 1
def test_add_user(init_database):
"""测试添加用户:依赖 init_database Fixture"""
print(f"使用数据库连接:{init_database}")
# 模拟添加操作
new_user = {"name": "李四"}
assert new_user["name"] == "李四"
执行命令 pytest test_db.py -s(-s 用于显示打印信息),输出如下:
collected 2 items
test_db.py
=== 初始化数据库连接 ===
使用数据库连接:模拟数据库连接对象
.
=== 关闭数据库连接 ===
=== 初始化数据库连接 ===
使用数据库连接:模拟数据库连接对象
.
=== 关闭数据库连接 ===
可见,每个用例执行前都会触发 Fixture 的前置操作,执行后触发后置操作,实现了重复操作的复用。
(2)Fixture 作用域:控制生效范围
通过 scope 参数可设置 Fixture 的作用域,减少重复执行次数,提升测试效率。常用作用域如下:
示例:设置 class 作用域
# conftest.py
@pytest.fixture(scope="class")
def init_class_database():
"""Fixture:class 作用域,每个测试类执行一次"""
print("\n=== 初始化 class 级数据库连接 ===")
db_conn = "class 级数据库连接对象"
yield db_conn
print("\n=== 关闭 class 级数据库连接 ===")
在测试类中使用该 Fixture:
# test_class_db.py
class TestUserDB:
def test_query_user(self, init_class_database):
print(f"class 级连接:{init_class_database}")
assert True
def test_add_user(self, init_class_database):
print(f"class 级连接:{init_class_database}")
assert True
执行命令 pytest test_class_db.py -s,输出如下:
collected 2 items
test_class_db.py
=== 初始化 class 级数据库连接 ===
class 级连接:class 级数据库连接对象
.
class 级连接:class 级数据库连接对象
.
=== 关闭 class 级数据库连接 ===
可见,两个用例共用了一次 Fixture 的前置 / 后置操作,减少了重复执行。
5. 用例控制:跳过、标记与失败重试
(1)跳过用例:@pytest.mark.skip
当某些用例暂时不需要执行(如功能未开发完成、环境不支持)时,可使用 @pytest.mark.skip 装饰器跳过,支持添加跳过原因。
# test_skip.py
import pytest
@pytest.mark.skip(reason="功能未开发完成,暂不测试")
def test_unfinished_feature():
"""测试未开发完成的功能"""
assert False # 即使断言失败,也不会执行
@pytest.mark.skipif(condition=True, reason="Python 版本小于 3.8,不支持该功能")
def test_python_version():
"""根据条件跳过(如版本判断)"""
assert True
执行后,跳过的用例会标记为 s(skipped):
collected 2 items
test_skip.py::test_unfinished_feature SKIPPED (功能未开发完成,暂不测试)
test_skip.py::test_python_version SKIPPED (Python 版本小于 3.8,不支持该功能)
(2)标记用例:@pytest.mark.自定义标记
当测试用例数量较多时,可通过自定义标记(如 smoke(冒烟测试)、api(接口测试)、ui(UI 测试))对用例分类,执行时只需指定标记即可筛选用例。
首先,在 pytest.ini 文件(需手动创建,与测试文件同级)中注册标记,避免警告:
# pytest.ini
[pytest]
markers =
smoke: 冒烟测试用例(核心功能)
api: 接口测试用例
ui: UI 测试用例
然后,在测试用例中添加标记:
# test_mark.py
import pytest
@pytest.mark.smoke # 标记为冒烟测试用例
def test_login_smoke():
"""登录功能冒烟测试(核心用例)"""
assert True
@pytest.mark.api # 标记为接口测试用例
def test_user_api():
"""用户接口测试"""</doubaocanvas>