Pytest 入门完全指南
1. 什么是 Pytest
1.1 Pytest 简介
Pytest 是 Python 中最流行的测试框架之一,它简单易用、功能强大,被广泛应用于单元测试、功能测试、集成测试和接口测试等场景。
Pytest 的核心特点:
- 简洁:使用简单的
assert语句进行断言,无需学习复杂的断言方法 - 易上手:测试函数只需以
test_开头即可被发现和执行 - 功能强大:支持参数化、Fixture、插件等高级特性
- 信息丰富:测试失败时提供详细的错误信息
- 插件丰富:拥有大量第三方插件,可扩展性强
1.2 Pytest vs Unittest
Python 内置了 unittest 测试框架,但 pytest 相比 unittest 有很多优势:
| 对比项 | Pytest | Unittest |
|---|---|---|
| 断言方式 | 使用简单的 assert |
需要使用 self.assertEqual() 等方法 |
| 测试发现 | 自动发现 test_ 开头的函数 |
需要继承 TestCase 类 |
| 代码量 | 更少、更简洁 | 较多、需要更多样板代码 |
| Fixture | 强大灵活的 fixture 系统 | 只有 setUp 和 tearDown |
| 参数化 | 内置参数化支持 | 需要额外库支持 |
| 插件生态 | 丰富的插件生态 | 插件较少 |
| 兼容性 | 可以运行 unittest 测试 | 不能运行 pytest 测试 |
Unittest 示例(繁琐):
import unittest
class TestMath(unittest.TestCase):
def test_addition(self):
self.assertEqual(1 + 1, 2)
def test_subtraction(self):
self.assertEqual(5 - 3, 2)
if __name__ == '__main__':
unittest.main()
Pytest 示例(简洁):
def test_addition():
assert 1 + 1 == 2
def test_subtraction():
assert 5 - 3 == 2
1.3 Pytest 的应用场景
Pytest 可以用于以下测试场景:
- 单元测试:测试单个函数或方法的功能
- 集成测试:测试多个模块之间的协作
- 接口测试(API 测试):测试 Web API 的功能和性能
- UI 自动化测试:配合 Selenium、Playwright 等工具进行 UI 测试
- 功能测试:测试软件的完整功能
- 回归测试:确保修改后的代码没有破坏现有功能
2. 安装 Pytest
2.1 安装前准备
在安装 pytest 之前,请确保你已经安装了 Python。可以在命令行中运行以下命令检查:
# 检查 Python 版本
python --version
# 或者
python3 --version
输出示例:
Python 3.9.0
注意:Pytest 支持 Python 3.7 及以上版本。
2.2 使用 pip 安装
pip 是 Python 的包管理工具,使用它可以轻松安装 pytest。
安装命令:
# Windows
pip install pytest
# Mac/Linux
pip3 install pytest
# 如果遇到权限问题,可以使用
pip install pytest --user
安装过程输出示例:
Collecting pytest
Downloading pytest-7.4.0-py3-none-any.whl (323 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 323.3/323.3 kB 5.0 MB/s eta 0:00:00
Installing collected packages: pytest
Successfully installed pytest-7.4.0
2.3 验证安装
安装完成后,验证 pytest 是否安装成功:
# 查看 pytest 版本
pytest --version
输出示例:
pytest 7.4.0
如果显示版本号,说明安装成功!
2.4 在虚拟环境中安装(推荐)
为什么使用虚拟环境?
- 隔离项目依赖,避免不同项目之间的依赖冲突
- 保持系统 Python 环境干净
- 便于管理项目依赖
创建和使用虚拟环境:
# 第一步:创建虚拟环境
python -m venv venv
# 第二步:激活虚拟环境
# Windows
venvScriptsactivate
# Mac/Linux
source venv/bin/activate
# 第三步:在虚拟环境中安装 pytest
pip install pytest
# 第四步:验证安装
pytest --version
激活虚拟环境后的命令行提示符:
(venv) C:UsersAdministratorDesktopproject>
注意命令行前面的 (venv) 表示虚拟环境已激活。
2.5 安装常用的 pytest 插件
Pytest 有很多有用的插件,以下是一些常用的:
# 生成 HTML 测试报告
pip install pytest-html
# 分布式执行测试(多进程并行)
pip install pytest-xdist
# 测试排序
pip install pytest-ordering
# 失败重试
pip install pytest-rerunfailures
# 生成 Allure 报告
pip install allure-pytest
# 一次性安装多个插件
pip install pytest pytest-html pytest-xdist pytest-rerunfailures
2.6 使用 requirements.txt 管理依赖
在项目中,推荐使用 requirements.txt 文件管理依赖:
创建 requirements.txt:
pytest==7.4.0
pytest-html==4.0.0
pytest-xdist==3.3.0
requests==2.31.0
安装依赖:
pip install -r requirements.txt
3. 编写第一个测试用例
3.1 创建测试文件
Pytest 会自动发现以 test_ 开头或以 _test 结尾的测试文件。
创建文件:test_first.py
# test_first.py
# 这是你的第一个 pytest 测试文件
def test_hello():
"""测试:Hello World"""
assert True # 断言为真,测试通过
def test_addition():
"""测试:加法运算"""
result = 1 + 1
assert result == 2 # 断言 1 + 1 等于 2
def test_string():
"""测试:字符串操作"""
message = "Hello, Pytest!"
assert "Pytest" in message # 断言 message 中包含 "Pytest"
3.2 运行测试
打开命令行,进入测试文件所在目录,运行:
pytest test_first.py
输出示例:
======================== test session starts ========================
platform win32 -- Python 3.9.0, pytest-7.4.0, pluggy-1.0.0
rootdir: C:UsersAdministratorDesktoppython自动化测试
collected 3 items
test_first.py ... [100%]
======================== 3 passed in 0.02s ========================
输出解释:
platform win32:运行平台(Windows)Python 3.9.0, pytest-7.4.0:Python 和 pytest 版本collected 3 items:发现了 3 个测试用例test_first.py ...:3 个点表示 3 个测试都通过了3 passed in 0.02s:3 个测试通过,耗时 0.02 秒
3.3 查看详细输出
使用 -v 参数查看更详细的输出:
pytest -v test_first.py
输出示例:
======================== test session starts ========================
platform win32 -- Python 3.9.0, pytest-7.4.0, pluggy-1.0.0
rootdir: C:UsersAdministratorDesktoppython自动化测试
collected 3 items
test_first.py::test_hello PASSED [ 33%]
test_first.py::test_addition PASSED [ 66%]
test_first.py::test_string PASSED [100%]
======================== 3 passed in 0.02s ========================
现在可以看到每个测试用例的名称和状态!
3.4 测试状态符号
在 pytest 的输出中,每个测试用例的状态用一个字符表示:
| 符号 | 含义 | 说明 |
|---|---|---|
. |
PASSED | 测试通过 |
F |
FAILED | 测试失败 |
E |
ERROR | 测试出错(代码有异常) |
s |
SKIPPED | 测试被跳过 |
x |
XFAIL | 预期失败的测试确实失败了 |
X |
XPASS | 预期失败的测试却通过了 |
示例:
test_example.py ..F.s [100%]
表示:2 个通过,1 个失败,1 个通过,1 个跳过
4. 理解断言(Assert)
4.1 什么是断言
断言(Assert) 是测试中用来验证结果是否符合预期的语句。如果断言成功,测试通过;如果断言失败,测试失败。
在 pytest 中,使用 Python 内置的 assert 语句进行断言,这比其他测试框架的断言方法更加简洁直观。
4.2 基本断言用法
# test_assert.py
def test_equal():
"""测试:相等"""
assert 1 + 1 == 2
def test_not_equal():
"""测试:不相等"""
assert 1 + 1 != 3
def test_greater():
"""测试:大于"""
assert 5 > 3
def test_less():
"""测试:小于"""
assert 3 < 5
def test_true():
"""测试:为真"""
assert True
def test_false():
"""测试:为假"""
assert not False
def test_in():
"""测试:包含"""
assert "hello" in "hello world"
def test_not_in():
"""测试:不包含"""
assert "goodbye" not in "hello world"
def test_is_none():
"""测试:为 None"""
result = None
assert result is None
def test_is_not_none():
"""测试:不为 None"""
result = "value"
assert result is not None
4.3 常见断言类型
4.3.1 相等断言
def test_equality():
# 数字相等
assert 1 + 1 == 2
# 字符串相等
assert "hello" == "hello"
# 列表相等
assert [1, 2, 3] == [1, 2, 3]
# 字典相等
assert {"name": "张三"} == {"name": "张三"}
4.3.2 布尔断言
def test_boolean():
# 断言为真
assert True
assert 1 # 非零数字为真
assert "hello" # 非空字符串为真
assert [1, 2] # 非空列表为真
# 断言为假(使用 not)
assert not False
assert not 0 # 0 为假
assert not "" # 空字符串为假
assert not [] # 空列表为假
4.3.3 包含断言
def test_contains():
# 字符串包含
assert "pytest" in "learning pytest is fun"
# 列表包含
assert 2 in [1, 2, 3]
# 字典包含键
assert "name" in {"name": "张三", "age": 25}
4.3.4 类型断言
def test_type():
# 检查类型
assert isinstance(1, int)
assert isinstance("hello", str)
assert isinstance([1, 2, 3], list)
assert isinstance({"key": "value"}, dict)
# 检查不是某类型
assert not isinstance("hello", int)
4.3.5 比较断言
def test_comparison():
# 大于
assert 10 > 5
# 大于等于
assert 10 >= 10
# 小于
assert 5 < 10
# 小于等于
assert 5 <= 5
# 近似相等(浮点数比较)
import math
assert math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9)
4.4 断言失败时的输出
当断言失败时,pytest 会显示详细的错误信息,帮助你快速定位问题。
测试文件:
# test_fail.py
def test_fail_example():
"""这个测试会失败"""
result = 1 + 1
assert result == 3, "1 + 1 应该等于 2,不是 3"
运行测试:
pytest -v test_fail.py
输出示例:
======================== test session starts ========================
platform win32 -- Python 3.9.0, pytest-7.4.0
collected 1 item
test_fail.py::test_fail_example FAILED [100%]
======================== FAILURES ========================
________________________ test_fail_example ________________________
def test_fail_example():
"""这个测试会失败"""
result = 1 + 1
> assert result == 3, "1 + 1 应该等于 2,不是 3"
E AssertionError: 1 + 1 应该等于 2,不是 3
E assert 2 == 3
test_fail.py:5: AssertionError
======================== 1 failed in 0.05s ========================
输出解释:
>表示断言失败的代码行E表示错误信息assert 2 == 3显示了实际值(2)和期望值(3)- 自定义消息 “1 + 1 应该等于 2,不是 3” 也会显示
4.5 添加断言消息
可以在断言后添加自定义消息,当断言失败时会显示:
def test_with_message():
age = 17
assert age >= 18, f"年龄必须大于等于 18,当前年龄是 {age}"
断言失败时的输出:
E AssertionError: 年龄必须大于等于 18,当前年龄是 17
E assert 17 >= 18
4.6 复杂断言示例
def test_complex_assertions():
# 用户数据
user = {
"name": "张三",
"age": 25,
"email": "zhangsan@example.com",
"roles": ["user", "admin"]
}
# 断言用户名正确
assert user["name"] == "张三", "用户名不匹配"
# 断言年龄在有效范围内
assert 0 < user["age"] < 120, "年龄不在有效范围内"
# 断言邮箱格式正确(简单检查)
assert "@" in user["email"], "邮箱格式不正确"
# 断言用户有 admin 角色
assert "admin" in user["roles"], "用户没有 admin 角色"
# 断言角色列表长度
assert len(user["roles"]) == 2, "角色数量不正确"
5. 测试函数的命名规则
5.1 文件命名规则
Pytest 会自动发现符合以下命名规则的测试文件:
| 命名规则 | 示例 | 是否会被发现 |
|---|---|---|
test_*.py |
test_login.py |
✅ 是 |
*_test.py |
login_test.py |
✅ 是 |
*.py(其他) |
login.py |
❌ 否 |
推荐使用 test_ 前缀:
project/
├── test_login.py ✅ 推荐
├── test_user.py ✅ 推荐
├── login_test.py ✅ 可以
└── login.py ❌ 不会被发现
5.2 函数命名规则
测试函数必须以 test_ 开头:
# test_example.py
# ✅ 正确:以 test_ 开头
def test_login():
assert True
def test_user_creation():
assert True
def test_api_response():
assert True
# ❌ 错误:不以 test_ 开头(不会被发现)
def login():
assert True
def check_user():
assert True
5.3 类命名规则
测试类必须以 Test 开头,且不能有 __init__ 方法:
# test_class.py
# ✅ 正确:类名以 Test 开头
class TestLogin:
def test_user_login(self):
assert True
def test_admin_login(self):
assert True
# ❌ 错误:类名不以 Test 开头
class LoginTest:
def test_login(self):
assert True # 不会被发现
# ❌ 错误:有 __init__ 方法
class TestUser:
def __init__(self):
self.name = "test"
def test_user(self):
assert True # 不会被发现
5.4 命名最佳实践
好的命名方式:
# 描述性命名,清晰表达测试内容
def test_user_login_with_valid_credentials():
"""测试用户使用有效凭据登录"""
pass
def test_user_login_with_invalid_password():
"""测试用户使用无效密码登录"""
pass
def test_user_registration_with_existing_email():
"""测试用户使用已存在的邮箱注册"""
pass
不好的命名方式:
# ❌ 不够描述性
def test_1():
pass
def test_login1():
pass
def test_a():
pass
6. 运行测试的多种方式
6.1 运行所有测试
# 运行当前目录及子目录下的所有测试
pytest
# 详细模式
pytest -v
6.2 运行指定文件
# 运行单个文件
pytest test_login.py
# 运行多个文件
pytest test_login.py test_user.py
6.3 运行指定目录
# 运行 tests 目录下的所有测试
pytest tests/
# 运行多个目录
pytest tests/ api_tests/
6.4 运行指定的测试函数
# 运行指定文件中的指定函数
pytest test_login.py::test_user_login
# 使用 -k 参数按名称筛选
pytest -k "login" # 运行所有名称包含 "login" 的测试
6.5 运行指定的测试类
# 运行指定文件中的指定类
pytest test_user.py::TestUser
# 运行类中的指定方法
pytest test_user.py::TestUser::test_create_user
6.6 使用表达式筛选测试
# 运行名称包含 "login" 的测试
pytest -k "login"
# 运行名称包含 "login" 且包含 "user" 的测试
pytest -k "login and user"
# 运行名称包含 "login" 或 "register" 的测试
pytest -k "login or register"
# 运行名称包含 "login" 但不包含 "admin" 的测试
pytest -k "login and not admin"
6.7 常用命令行参数
| 参数 | 作用 | 示例 |
|---|---|---|
-v |
详细输出 | pytest -v |
-s |
显示 print 输出 | pytest -s |
-x |
遇到失败立即停止 | pytest -x |
-q |
简洁输出 | pytest -q |
--tb=short |
简短的错误信息 | pytest --tb=short |
--collect-only |
只收集测试,不运行 | pytest --collect-only |
6.8 组合使用参数
# 详细输出 + 显示 print + 遇到失败停止
pytest -xvs
# 详细输出 + 简短错误信息
pytest -v --tb=short
# 只运行包含 "login" 的测试,详细输出
pytest -k "login" -v
7. 项目结构组织
7.1 基本项目结构
my_project/
├── src/ # 源代码目录
│ ├── __init__.py
│ ├── calculator.py
│ └── user.py
├── tests/ # 测试目录
│ ├── __init__.py
│ ├── test_calculator.py
│ └── test_user.py
├── pytest.ini # pytest 配置文件
├── requirements.txt # 项目依赖
└── README.md
7.2 源代码示例
src/calculator.py:
# src/calculator.py
class Calculator:
"""简单的计算器类"""
def add(self, a, b):
"""加法"""
return a + b
def subtract(self, a, b):
"""减法"""
return a - b
def multiply(self, a, b):
"""乘法"""
return a * b
def divide(self, a, b):
"""除法"""
if b == 0:
raise ValueError("除数不能为零")
return a / b
7.3 测试代码示例
tests/test_calculator.py:
# tests/test_calculator.py
import pytest
import sys
sys.path.insert(0, '../src') # 添加源代码路径
from calculator import Calculator
class TestCalculator:
"""计算器测试类"""
def setup_method(self):
"""每个测试方法执行前调用"""
self.calc = Calculator()
def test_add(self):
"""测试加法"""
result = self.calc.add(1, 2)
assert result == 3
def test_add_negative(self):
"""测试负数加法"""
result = self.calc.add(-1, -2)
assert result == -3
def test_subtract(self):
"""测试减法"""
result = self.calc.subtract(5, 3)
assert result == 2
def test_multiply(self):
"""测试乘法"""
result = self.calc.multiply(3, 4)
assert result == 12
def test_divide(self):
"""测试除法"""
result = self.calc.divide(10, 2)
assert result == 5
def test_divide_by_zero(self):
"""测试除以零"""
with pytest.raises(ValueError):
self.calc.divide(10, 0)
7.4 pytest.ini 配置文件
pytest.ini:
[pytest]
# 测试文件匹配模式
python_files = test_*.py
# 测试类匹配模式
python_classes = Test*
# 测试函数匹配模式
python_functions = test_*
# 测试目录
testpaths = tests
# 默认命令行参数
addopts = -v --tb=short
# 标记定义
markers =
smoke: 冒烟测试
integration: 集成测试
slow: 慢速测试
7.5 运行项目测试
# 进入项目目录
cd my_project
# 运行所有测试
pytest
# 运行特定测试文件
pytest tests/test_calculator.py
# 运行详细模式
pytest -v
8. Setup 和 Teardown
8.1 什么是 Setup 和 Teardown
Setup(前置):在测试执行之前运行的代码,用于准备测试环境。
Teardown(后置):在测试执行之后运行的代码,用于清理测试环境。
8.2 函数级别的 Setup 和 Teardown
# test_setup_teardown.py
def setup_function():
"""每个测试函数执行前调用"""
print("n--- 函数级别 Setup ---")
def teardown_function():
"""每个测试函数执行后调用"""
print("n--- 函数级别 Teardown ---")
def test_example_1():
print("执行测试 1")
assert True
def test_example_2():
print("执行测试 2")
assert True
运行测试(使用 -s 查看 print 输出):
pytest -sv test_setup_teardown.py
输出示例:
test_setup_teardown.py::test_example_1
--- 函数级别 Setup ---
执行测试 1
PASSED
--- 函数级别 Teardown ---
test_setup_teardown.py::test_example_2
--- 函数级别 Setup ---
执行测试 2
PASSED
--- 函数级别 Teardown ---
8.3 类级别的 Setup 和 Teardown
# test_class_setup.py
class TestExample:
@classmethod
def setup_class(cls):
"""整个类执行前调用一次"""
print("n=== 类级别 Setup ===")
cls.shared_data = "共享数据"
@classmethod
def teardown_class(cls):
"""整个类执行后调用一次"""
print("n=== 类级别 Teardown ===")
def setup_method(self):
"""每个测试方法执行前调用"""
print("n--- 方法级别 Setup ---")
def teardown_method(self):
"""每个测试方法执行后调用"""
print("n--- 方法级别 Teardown ---")
def test_method_1(self):
print(f"执行测试 1,共享数据:{self.shared_data}")
assert True
def test_method_2(self):
print(f"执行测试 2,共享数据:{self.shared_data}")
assert True
输出示例:
=== 类级别 Setup ===
--- 方法级别 Setup ---
执行测试 1,共享数据:共享数据
PASSED
--- 方法级别 Teardown ---
--- 方法级别 Setup ---
执行测试 2,共享数据:共享数据
PASSED
--- 方法级别 Teardown ---
=== 类级别 Teardown ===
8.4 模块级别的 Setup 和 Teardown
# test_module_setup.py
def setup_module():
"""整个模块(文件)执行前调用一次"""
print("n>>> 模块级别 Setup <<<")
def teardown_module():
"""整个模块(文件)执行后调用一次"""
print("n>>> 模块级别 Teardown <<<")
def test_1():
print("执行测试 1")
assert True
def test_2():
print("执行测试 2")
assert True
8.5 执行顺序
模块 Setup
└── 类 Setup
└── 方法 Setup
└── 测试执行
└── 方法 Teardown
└── 类 Teardown
模块 Teardown
8.6 实际应用场景
场景 1:数据库测试
class TestDatabase:
@classmethod
def setup_class(cls):
"""创建数据库连接"""
cls.db = DatabaseConnection()
cls.db.connect()
@classmethod
def teardown_class(cls):
"""关闭数据库连接"""
cls.db.disconnect()
def setup_method(self):
"""每个测试前开启事务"""
self.db.begin_transaction()
def teardown_method(self):
"""每个测试后回滚事务"""
self.db.rollback()
def test_insert_user(self):
self.db.insert_user({"name": "张三"})
assert self.db.get_user_count() == 1
场景 2:浏览器自动化测试
class TestBrowser:
@classmethod
def setup_class(cls):
"""启动浏览器"""
cls.driver = webdriver.Chrome()
@classmethod
def teardown_class(cls):
"""关闭浏览器"""
cls.driver.quit()
def test_home_page(self):
self.driver.get("https://example.com")
assert "Example" in self.driver.title
9. 跳过测试和预期失败
9.1 跳过测试(skip)
使用 @pytest.mark.skip 装饰器跳过测试:
import pytest
@pytest.mark.skip(reason="功能尚未实现")
def test_not_implemented():
"""这个测试会被跳过"""
assert False
@pytest.mark.skip
def test_skip_without_reason():
"""跳过但不提供原因"""
assert False
运行结果:
test_skip.py::test_not_implemented SKIPPED (功能尚未实现)
test_skip.py::test_skip_without_reason SKIPPED
9.2 条件跳过(skipif)
根据条件跳过测试:
import pytest
import sys
@pytest.mark.skipif(sys.version_info < (3, 9), reason="需要 Python 3.9+")
def test_python_39_feature():
"""只在 Python 3.9+ 上运行"""
assert True
@pytest.mark.skipif(sys.platform == 'win32', reason="不支持 Windows")
def test_linux_only():
"""只在 Linux 上运行"""
assert True
# 可以定义条件变量
is_database_available = False
@pytest.mark.skipif(not is_database_available, reason="数据库不可用")
def test_database():
"""需要数据库连接"""
assert True
9.3 预期失败(xfail)
使用 @pytest.mark.xfail 标记预期会失败的测试:
import pytest
@pytest.mark.xfail(reason="已知 bug,等待修复")
def test_known_bug():
"""这个测试预期会失败"""
assert 1 + 1 == 3
@pytest.mark.xfail(reason="功能待实现")
def test_todo():
"""待实现的功能"""
raise NotImplementedError
运行结果:
test_xfail.py::test_known_bug XFAIL (已知 bug,等待修复)
test_xfail.py::test_todo XFAIL (功能待实现)
9.4 跳过模块中的所有测试
import pytest
# 跳过整个模块
pytestmark = pytest.mark.skip(reason="整个模块跳过")
def test_1():
assert True
def test_2():
assert True
9.5 在代码中动态跳过
import pytest
def test_with_condition():
database_available = check_database()
if not database_available:
pytest.skip("数据库不可用,跳过测试")
# 实际测试代码
assert True
10. 测试异常
10.1 使用 pytest.raises
测试代码是否抛出预期的异常:
import pytest
def divide(a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
def test_divide_by_zero():
"""测试除以零是否抛出异常"""
with pytest.raises(ValueError):
divide(10, 0)
10.2 检查异常消息
import pytest
def test_exception_message():
"""测试异常消息"""
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert str(exc_info.value) == "除数不能为零"
# 或者使用 match 参数
with pytest.raises(ValueError, match="除数不能为零"):
divide(10, 0)
10.3 使用正则表达式匹配
import pytest
def test_exception_with_regex():
"""使用正则表达式匹配异常消息"""
with pytest.raises(ValueError, match=r"除数.*零"):
divide(10, 0)
10.4 测试多种异常类型
import pytest
def risky_function(value):
if value < 0:
raise ValueError("值不能为负")
if value > 100:
raise OverflowError("值太大")
return value
def test_multiple_exceptions():
"""测试多种异常"""
# 测试 ValueError
with pytest.raises(ValueError):
risky_function(-1)
# 测试 OverflowError
with pytest.raises(OverflowError):
risky_function(101)
10.5 测试不抛出异常
def test_no_exception():
"""测试不抛出异常的情况"""
try:
result = divide(10, 2)
assert result == 5
except Exception as e:
pytest.fail(f"意外的异常:{e}")
11. 参数化测试
11.1 什么是参数化测试
参数化测试允许你使用不同的参数多次运行同一个测试函数,减少重复代码。
11.2 基本用法
import pytest
@pytest.mark.parametrize("input,expected", [
(1 + 1, 2),
(2 + 2, 4),
(3 + 3, 6),
])
def test_addition(input, expected):
"""参数化测试加法"""
assert input == expected
运行结果:
test_param.py::test_addition[2-2] PASSED
test_param.py::test_addition[4-4] PASSED
test_param.py::test_addition[6-6] PASSED
11.3 多参数示例
import pytest
@pytest.mark.parametrize("username,password,expected", [
("admin", "123456", True),
("user", "password", True),
("admin", "wrong", False),
("", "123456", False),
])
def test_login(username, password, expected):
"""参数化测试登录"""
result = login(username, password) # 假设有这个函数
assert result == expected
11.4 使用 ID 标识测试用例
import pytest
@pytest.mark.parametrize("a,b,expected", [
pytest.param(1, 2, 3, id="正数相加"),
pytest.param(-1, -2, -3, id="负数相加"),
pytest.param(0, 0, 0, id="零相加"),
])
def test_add_with_ids(a, b, expected):
"""带 ID 的参数化测试"""
assert a + b == expected
运行结果:
test_param.py::test_add_with_ids[正数相加] PASSED
test_param.py::test_add_with_ids[负数相加] PASSED
test_param.py::test_add_with_ids[零相加] PASSED
11.5 组合多个参数化
import pytest
@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [10, 20])
def test_multiply(x, y):
"""组合参数化"""
print(f"x={x}, y={y}")
assert x * y > 0
# 这会生成 4 个测试:
# (1, 10), (1, 20), (2, 10), (2, 20)
11.6 从文件读取参数
import pytest
import json
# 从 JSON 文件读取测试数据
def load_test_data():
with open("test_data.json", "r") as f:
return json.load(f)
@pytest.mark.parametrize("username,password,expected", load_test_data())
def test_login_from_file(username, password, expected):
"""从文件读取参数"""
result = login(username, password)
assert result == expected
12. 简单的 Fixture 入门
12.1 什么是 Fixture
Fixture 是 pytest 中用于准备测试数据和环境的强大机制。它可以在测试之前设置所需的数据,并在测试之后进行清理。
12.2 基本用法
import pytest
@pytest.fixture
def sample_data():
"""提供测试数据的 fixture"""
return {"name": "张三", "age": 25}
def test_user_name(sample_data):
"""使用 fixture"""
assert sample_data["name"] == "张三"
def test_user_age(sample_data):
"""多个测试可以使用同一个 fixture"""
assert sample_data["age"] == 25
12.3 Fixture 的 Setup 和 Teardown
使用 yield 分隔 setup 和 teardown 逻辑:
import pytest
@pytest.fixture
def database():
"""数据库 fixture"""
# Setup:测试前执行
print("n连接数据库...")
db = DatabaseConnection()
db.connect()
yield db # 返回 fixture 值给测试使用
# Teardown:测试后执行
print("n断开数据库...")
db.disconnect()
def test_query(database):
"""使用数据库 fixture"""
result = database.query("SELECT * FROM users")
assert result is not None
12.4 Fixture 作用域
import pytest
@pytest.fixture(scope="function") # 默认,每个测试函数都会执行
def function_scope():
print("n函数级别 fixture")
return "function"
@pytest.fixture(scope="class") # 每个测试类执行一次
def class_scope():
print("n类级别 fixture")
return "class"
@pytest.fixture(scope="module") # 每个模块执行一次
def module_scope():
print("n模块级别 fixture")
return "module"
@pytest.fixture(scope="session") # 整个测试会话执行一次
def session_scope():
print("n会话级别 fixture")
return "session"
12.5 自动使用 Fixture
import pytest
@pytest.fixture(autouse=True)
def setup_logging():
"""自动应用的 fixture"""
print("n开始测试...")
yield
print("n测试结束")
def test_1():
assert True
def test_2():
assert True
# 两个测试都会自动应用 setup_logging fixture
12.6 conftest.py 中共享 Fixture
在 conftest.py 文件中定义的 fixture 可以被同目录及子目录的测试使用:
conftest.py:
import pytest
@pytest.fixture
def shared_data():
"""共享的 fixture"""
return {"shared": True}
@pytest.fixture
def api_client():
"""API 客户端 fixture"""
from api_client import APIClient
return APIClient(base_url="https://api.example.com")
test_example.py:
def test_with_shared_data(shared_data):
"""使用共享的 fixture"""
assert shared_data["shared"] == True
def test_api(api_client):
"""使用 API 客户端 fixture"""
response = api_client.get("/users")
assert response.status_code == 200
13. 常用的 Pytest 命令
13.1 基础命令
# 运行所有测试
pytest
# 详细模式
pytest -v
# 显示 print 输出
pytest -s
# 组合:详细模式 + 显示 print
pytest -vs
# 遇到第一个失败就停止
pytest -x
# 运行上次失败的测试
pytest --lf
# 运行所有测试,但先运行上次失败的
pytest --ff
13.2 筛选测试
# 运行包含 "login" 的测试
pytest -k "login"
# 运行标记为 smoke 的测试
pytest -m smoke
# 只运行某个文件
pytest test_login.py
# 只运行某个函数
pytest test_login.py::test_user_login
# 只运行某个类
pytest test_login.py::TestLogin
13.3 输出控制
# 简洁输出
pytest -q
# 显示更详细的输出
pytest -vv
# 不显示错误回溯
pytest --tb=no
# 简短的错误回溯
pytest --tb=short
# 只显示一行错误信息
pytest --tb=line
13.4 测试收集
# 只收集测试,不运行
pytest --collect-only
# 显示收集的测试详情
pytest --collect-only -v
13.5 并行执行(需要 pytest-xdist)
# 使用 4 个进程并行执行
pytest -n 4
# 自动选择 CPU 数量
pytest -n auto
13.6 生成报告(需要 pytest-html)
# 生成 HTML 报告
pytest --html=report.html
# 生成独立的 HTML 报告(包含样式)
pytest --html=report.html --self-contained-html
14. 常见问题与解决方案
14.1 测试文件找不到
问题:pytest 运行时提示 “no tests ran”
原因:
- 文件名不符合
test_*.py或*_test.py规则 - 函数名不以
test_开头
解决方案:
# ❌ 错误的文件名
login.py
# ✅ 正确的文件名
test_login.py
# ❌ 错误的函数名
def login():
pass
# ✅ 正确的函数名
def test_login():
pass
14.2 模块导入错误
问题:ModuleNotFoundError: No module named 'xxx'
解决方案:
- 在项目根目录添加
conftest.py(空文件即可) - 使用相对导入或绝对导入
- 配置 Python 路径
# conftest.py(放在项目根目录)
import sys
import os
# 将项目根目录添加到 Python 路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14.3 print 输出不显示
问题:测试中的 print() 语句没有输出
解决方案:使用 -s 参数
pytest -s test_example.py
14.4 类中有 init 方法导致测试不被发现
问题:测试类中有 __init__ 方法,测试不被收集
解决方案:使用 setup_method 或 fixture 代替 __init__
# ❌ 错误:使用 __init__
class TestUser:
def __init__(self):
self.user = User()
def test_user(self):
assert self.user is not None
# ✅ 正确:使用 setup_method
class TestUser:
def setup_method(self):
self.user = User()
def test_user(self):
assert self.user is not None
# ✅ 正确:使用 fixture
class TestUser:
@pytest.fixture(autouse=True)
def setup(self):
self.user = User()
def test_user(self):
assert self.user is not None
14.5 测试顺序问题
问题:测试之间有依赖,需要按特定顺序执行
解决方案:
- 安装
pytest-ordering插件 - 使用
@pytest.mark.run(order=n)指定顺序
pip install pytest-ordering
import pytest
@pytest.mark.run(order=1)
def test_first():
pass
@pytest.mark.run(order=2)
def test_second():
pass
14.6 中文乱码问题
问题:输出中文时出现乱码
解决方案:
- 确保文件编码为 UTF-8
- 在文件开头添加编码声明
- Windows 下设置控制台编码
# -*- coding: utf-8 -*-
def test_chinese():
message = "你好,世界!"
assert "你好" in message
Windows 控制台:
chcp 65001
pytest -v
15. 最佳实践
15.1 测试命名
# ✅ 好的命名:描述性强
def test_user_login_with_valid_credentials_should_succeed():
pass
def test_user_login_with_invalid_password_should_fail():
pass
# ❌ 不好的命名:不够描述性
def test_1():
pass
def test_login():
pass
15.2 测试组织
# 使用类组织相关测试
class TestUserLogin:
"""用户登录相关测试"""
def test_login_success(self):
pass
def test_login_wrong_password(self):
pass
def test_login_user_not_found(self):
pass
class TestUserRegistration:
"""用户注册相关测试"""
def test_register_success(self):
pass
def test_register_duplicate_email(self):
pass
15.3 使用 Fixture 管理数据
import pytest
@pytest.fixture
def valid_user():
return {"username": "testuser", "password": "password123"}
@pytest.fixture
def invalid_user():
return {"username": "", "password": ""}
def test_login_with_valid_user(valid_user):
result = login(valid_user["username"], valid_user["password"])
assert result == True
def test_login_with_invalid_user(invalid_user):
result = login(invalid_user["username"], invalid_user["password"])
assert result == False
15.4 保持测试独立
# ✅ 好的做法:每个测试独立
def test_create_user():
user = create_user("test@example.com")
assert user is not None
delete_user(user.id) # 清理
def test_delete_user():
user = create_user("delete@example.com") # 自己准备数据
result = delete_user(user.id)
assert result == True
# ❌ 不好的做法:测试之间有依赖
user_id = None
def test_create_user():
global user_id
user = create_user("test@example.com")
user_id = user.id
assert user is not None
def test_delete_user():
# 依赖 test_create_user 先执行
result = delete_user(user_id)
assert result == True
15.5 使用标记分类测试
import pytest
@pytest.mark.smoke
def test_home_page():
"""冒烟测试:首页"""
pass
@pytest.mark.regression
def test_user_flow():
"""回归测试:用户流程"""
pass
@pytest.mark.slow
def test_performance():
"""性能测试(慢)"""
pass
# 只运行冒烟测试
pytest -m smoke
# 跳过慢速测试
pytest -m "not slow"
16. 总结
16.1 Pytest 核心概念回顾
- 测试发现:以
test_开头的文件和函数会被自动发现 - 断言:使用简单的
assert语句进行验证 - Fixture:用于准备测试数据和环境
- 参数化:使用不同参数多次运行同一测试
- 标记:对测试进行分类和筛选
16.2 常用命令速查
| 命令 | 作用 |
|---|---|
pytest |
运行所有测试 |
pytest -v |
详细输出 |
pytest -s |
显示 print |
pytest -x |
遇到失败停止 |
pytest -k "xxx" |
按名称筛选 |
pytest -m xxx |
按标记筛选 |
pytest --collect-only |
只收集不运行 |
16.3 学习路线
-
入门阶段(本文档)
- 安装 pytest
- 编写第一个测试
- 理解断言
- 运行测试
-
进阶阶段
- Fixture 深入学习
- 参数化测试
- 标记和跳过
- conftest.py
-
高级阶段
- 插件开发
- 接口自动化
- 与 CI/CD 集成
- 测试报告生成
16.4 推荐的下一步
学完本入门教程后,建议按以下顺序继续学习:
- 01.pytest的用例发现规则.md – 深入理解测试发现机制
- 02.pytest中常用的参数.md – 掌握更多命令行参数
- 07.pytest的fixture.md – 深入学习 Fixture
- 05.pytest中数据驱动测试.md – 学习数据驱动测试
附录
A. Pytest 官方资源
- 官方文档:https://docs.pytest.org/
- GitHub:https://github.com/pytest-dev/pytest
- 插件列表:https://docs.pytest.org/en/latest/reference/plugin_list.html
B. 常用插件列表
| 插件 | 作用 |
|---|---|
| pytest-html | 生成 HTML 报告 |
| pytest-xdist | 并行执行测试 |
| pytest-rerunfailures | 失败重试 |
| pytest-ordering | 测试排序 |
| pytest-cov | 代码覆盖率 |
| allure-pytest | Allure 报告 |
| pytest-mock | Mock 支持 |
| pytest-timeout | 超时控制 |
C. 快速开始模板
project/conftest.py:
import pytest
@pytest.fixture
def api_client():
"""API 客户端 fixture"""
# 在此初始化 API 客户端
pass
project/pytest.ini:
[pytest]
python_files = test_*.py
python_functions = test_*
python_classes = Test*
testpaths = tests
addopts = -v --tb=short
markers =
smoke: 冒烟测试
slow: 慢速测试
project/tests/test_example.py:
import pytest
class TestExample:
def test_hello(self):
assert True
@pytest.mark.smoke
def test_smoke(self):
assert True
恭喜你完成了 Pytest 入门学习!
现在你已经掌握了 pytest 的基础知识,可以开始编写自己的测试用例了。记住,实践是最好的学习方式,多写测试、多运行、多调试,你会越来越熟练!