# -*- coding: utf-8 -*-
"""
============================
pytest 大师课:从入门到精通
============================
版本:2.1 | 修复版
"""
# ====================
# 第一部分:pytest 核心概念
# ====================
"""
1. 为什么选择 pytest?
- 更简洁的测试代码(相比 unittest)
- 强大的断言机制(无需记忆特定方法)
- 灵活的夹具系统(fixtures)
- 丰富的插件生态(1000+ 插件)
- 智能测试发现与执行
2. 核心组件:
• 测试发现:自动查找 test_*.py 文件
• 测试执行:并行、选择性和顺序执行
• 报告系统:多种格式输出
• 插件架构:高度可扩展
3. 安装与升级:
pip install -U pytest pytest-html pytest-xdist pytest-cov pytest-mock
"""
# ====================
# 第二部分:基础到进阶示例
# ====================
import os # 导入操作系统接口模块,用于文件路径操作
import pytest # 导入pytest测试框架
import sys # 导入系统模块,用于访问Python解释器相关功能
from datetime import datetime # 导入日期时间模块,用于时间相关操作
from unittest.mock import MagicMock, patch # 导入mock模块,用于模拟对象和函数
# --------------------
# 1. 测试函数与断言
# --------------------
def test_addition():
"""基础断言示例:验证基本算术运算"""
assert 1 + 1 == 2, "加法计算错误" # 使用assert进行简单断言
def test_container_operations():
"""容器操作断言:验证列表操作"""
numbers = [1, 2, 3, 4, 5] # 定义测试数据列表
assert 3 in numbers # 检查元素是否在列表中
assert numbers == [1, 2, 3, 4, 5] # 检查列表是否相等
assert all(n > 0 for n in numbers) # 检查所有元素是否满足条件
assert any(n % 2 == 0 for n in numbers) # 检查是否有元素满足条件
def test_floating_point():
"""浮点数近似断言:处理浮点数精度问题"""
result = 0.1 + 0.2 # 计算0.1+0.2
assert result == pytest.approx(0.3, rel=1e-5) # 使用approx进行近似比较
# --------------------
# 2. 夹具(fixture)系统详解
# --------------------
@pytest.fixture(scope="module") # 定义模块级夹具,整个测试模块共享
def database_connection():
"""模块级夹具 - 整个测试模块共享"""
print("\n>>> 创建数据库连接 (模块级)") # 夹具初始化时打印信息
conn = {"status": "connected", "version": "1.2.3"} # 创建模拟数据库连接对象
yield conn # 返回连接对象,测试结束后继续执行清理
print("\n>>> 关闭数据库连接 (模块级)") # 夹具清理时打印信息
@pytest.fixture # 定义函数级夹具(默认作用域)
def temporary_user(database_connection): # 依赖database_connection夹具
"""函数级夹具 - 依赖其他夹具"""
print("\n>> 创建临时用户") # 夹具初始化时打印信息
user = {"id": 1001, "name": "test_user", "created_at": datetime.now()} # 创建模拟用户
yield user # 返回用户对象,测试结束后继续执行清理
print("\n>> 删除临时用户") # 夹具清理时打印信息
def test_user_creation(temporary_user): # 使用temporary_user夹具
"""测试用户创建:验证夹具功能"""
assert temporary_user["id"] == 1001 # 验证用户ID
assert "test" in temporary_user["name"] # 验证用户名包含特定字符串
# --------------------
# 3. 参数化高级用法 (修复版)
# --------------------
def is_prime(n):
"""判断质数的函数:检查数字是否为质数"""
if n < 2: # 小于2的数不是质数
return False
for i in range(2, int(n ** 0.5) + 1): # 只需检查到平方根
if n % i == 0: # 如果可整除则不是质数
return False
return True # 通过所有检查则是质数
# 修复的参数化测试
@pytest.mark.parametrize( # 参数化测试装饰器
"number, expected", # 参数名称
[ # 参数值列表
(2, True), # 普通参数
(3, True), # 普通参数
(4, False), # 普通参数
(17, True), # 普通参数
(25, False), # 普通参数
pytest.param(1, False, id="edge_case_1"), # 使用pytest.param定义特殊参数
pytest.param(0, False, id="edge_case_0"), # 使用pytest.param定义特殊参数
],
ids=lambda param: f"num_{param[0]}" if isinstance(param, tuple) else param.id # 智能ID生成函数
)
def test_prime_numbers(number, expected):
"""测试质数判断函数:验证is_prime函数"""
assert is_prime(number) == expected # 验证函数结果与预期一致
# --------------------
# 4. 标记系统高级应用
# --------------------
@pytest.mark.slow # 标记为慢速测试
@pytest.mark.integration # 标记为集成测试
def test_external_api_call():
"""集成测试:模拟外部API调用"""
import time # 导入时间模块
time.sleep(1.5) # 模拟API调用延迟
assert True # 简单断言
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要Python 3.8+") # 条件跳过装饰器
def test_walrus_operator():
"""测试海象运算符:验证Python 3.8+特性"""
data = [1, 2, 3, 4, 5] # 测试数据
if (n := len(data)) > 3: # 使用海象运算符
assert n == 5 # 验证结果
@pytest.mark.xfail(sys.platform == "win32", reason="Windows平台已知问题") # 预期失败装饰器
def test_filesystem_case_sensitivity():
"""测试文件系统大小写敏感性:验证不同OS行为差异"""
with open("tempfile.txt", "w") as f: # 创建临时文件
f.write("test") # 写入内容
assert not os.path.exists("TEMPFILE.txt") # Linux/Mac大小写敏感
os.remove("tempfile.txt") # 清理测试文件
# --------------------
# 5. 异常处理与警告
# --------------------
def test_expected_exception():
"""测试预期异常:验证异常处理"""
with pytest.raises(ValueError) as exc_info: # 捕获预期异常
int("not_a_number") # 故意引发异常的操作
assert "invalid literal" in str(exc_info.value) # 验证异常信息
def test_warnings():
"""测试警告捕获:验证警告处理"""
with pytest.warns(DeprecationWarning): # 捕获预期警告
import warnings # 导入警告模块
warnings.warn("This is deprecated", DeprecationWarning) # 发出警告
# --------------------
# 6. Mock与猴子补丁
# --------------------
def fetch_weather_data(api_url):
"""获取天气数据的函数:模拟外部API调用"""
# 实际实现会调用外部API
return {"temp": 25, "condition": "sunny"} # 返回模拟数据
def test_mock_external_api():
"""使用Mock替代外部API调用:验证mock功能"""
# 创建Mock替代实际函数
with patch(__name__ + ".fetch_weather_data") as mock_weather: # 使用patch上下文管理器
mock_weather.return_value = {"temp": 30, "condition": "cloudy"} # 设置mock返回值
result = fetch_weather_data("https://weather-api.com") # 调用被mock的函数
assert result["temp"] == 30 # 验证mock结果
mock_weather.assert_called_once_with("https://weather-api.com") # 验证调用参数
# --------------------
# 7. 临时文件与目录
# --------------------
def test_create_temp_files(tmp_path): # 使用pytest内置的tmp_path夹具
"""测试临时文件操作:验证文件系统功能"""
# 创建临时目录结构
data_dir = tmp_path / "data" # 创建子目录路径
data_dir.mkdir() # 创建目录
# 创建文件
file_path = data_dir / "test.csv" # 创建文件路径
file_path.write_text("id,name\n1,Alice\n2,Bob") # 写入文件内容
# 验证内容
content = file_path.read_text() # 读取文件内容
assert "Alice" in content # 验证内容包含Alice
assert "Bob" in content # 验证内容包含Bob
# ====================
# 第三部分:高级工程实践
# ====================
# --------------------
# 8. 测试覆盖率分析
# --------------------
"""
使用pytest-cov生成覆盖率报告:
1. 基本报告:pytest --cov=my_module
2. HTML报告:pytest --cov=my_module --cov-report=html
3. 阈值控制:pytest --cov=my_module --cov-fail-under=90
"""
# 被测代码
def calculate_discount(price, discount_percent):
"""计算折扣后的价格:业务逻辑函数"""
if discount_percent < 0 or discount_percent > 100: # 验证折扣率范围
raise ValueError("折扣率必须在0-100之间") # 抛出异常
return price * (1 - discount_percent / 100) # 计算折扣后价格
# 测试用例
def test_discount_calculation():
"""测试折扣计算:正常情况"""
assert calculate_discount(100, 20) == 80 # 验证20%折扣
assert calculate_discount(50, 10) == 45 # 验证10%折扣
def test_discount_edge_cases():
"""测试折扣计算:边界情况"""
with pytest.raises(ValueError): # 验证异常
calculate_discount(100, -5) # 负折扣率
with pytest.raises(ValueError): # 验证异常
calculate_discount(100, 110) # 超过100%的折扣率
# --------------------
# 9. 并行测试执行
# --------------------
"""
使用pytest-xdist进行并行测试:
1. 安装:pip install pytest-xdist
2. 运行:pytest -n auto # 自动检测CPU核心数
3. 指定核心数:pytest -n 4
注意:确保测试是独立的,无共享状态
"""
@pytest.mark.parametrize("x", range(10)) # 参数化测试,10组数据
def test_parallel_execution(x):
"""模拟大量可并行测试:验证并行执行能力"""
assert x * 0 == 0 # 简单断言
# --------------------
# 10. 自定义夹具参数化
# --------------------
def generate_test_data():
"""生成测试数据组合:创建参数化数据"""
return [ # 返回测试数据列表
{"input": "admin", "role": "administrator"}, # 管理员数据
{"input": "user", "role": "standard"}, # 普通用户数据
{"input": "guest", "role": "limited"} # 访客数据
]
@pytest.fixture(params=generate_test_data()) # 参数化夹具
def user_account(request): # request提供参数信息
"""参数化夹具:根据参数生成不同用户账户"""
return request.param # 返回当前参数
def test_user_roles(user_account): # 使用参数化夹具
"""测试不同用户角色:验证权限系统"""
assert user_account["role"] in ["administrator", "standard", "limited"] # 验证角色
# ====================
# 第六部分:完整测试示例
# ====================
class UserManagementSystem:
"""用户管理系统模拟类:业务逻辑封装"""
def __init__(self):
"""初始化用户系统"""
self.users = {} # 用户存储字典
self.next_id = 1 # 下一个用户ID
def add_user(self, name, email):
"""添加用户:创建新用户"""
if not email or "@" not in email: # 验证邮箱格式
raise ValueError("无效的邮箱地址") # 无效邮箱抛出异常
user_id = self.next_id # 分配用户ID
self.users[user_id] = {"name": name, "email": email} # 存储用户信息
self.next_id += 1 # ID自增
return user_id # 返回新用户ID
def get_user(self, user_id):
"""获取用户信息:根据ID查询用户"""
return self.users.get(user_id) # 返回用户信息或None
def delete_user(self, user_id):
"""删除用户:根据ID删除用户"""
if user_id not in self.users: # 检查用户是否存在
raise KeyError("用户不存在") # 不存在则抛出异常
del self.users[user_id] # 删除用户
@pytest.fixture # 定义用户系统夹具
def user_system():
"""用户管理系统夹具:创建预配置的用户系统"""
system = UserManagementSystem() # 创建用户系统实例
# 添加初始用户
system.add_user("Admin", "admin@example.com") # 添加管理员用户
return system # 返回系统实例
class TestUserManagement: # 测试类
"""用户管理系统测试套件:完整业务逻辑测试"""
def test_add_user(self, user_system): # 测试添加用户
"""测试添加用户:验证用户创建功能"""
user_id = user_system.add_user("Alice", "alice@example.com") # 添加新用户
assert user_id == 2 # 验证用户ID(初始用户已占1)
assert user_system.get_user(2)["name"] == "Alice" # 验证用户名
def test_add_invalid_email(self, user_system): # 测试无效邮箱
"""测试无效邮箱:验证异常处理"""
with pytest.raises(ValueError) as excinfo: # 捕获预期异常
user_system.add_user("Bob", "invalid-email") # 添加无效邮箱用户
assert "无效的邮箱地址" in str(excinfo.value) # 验证异常信息
def test_delete_user(self, user_system): # 测试删除用户
"""测试删除用户:验证用户删除功能"""
user_system.delete_user(1) # 删除初始用户
with pytest.raises(KeyError): # 捕获预期异常
user_system.get_user(1) # 尝试获取已删除用户
@pytest.mark.parametrize("email", [ # 参数化测试
"test@example.com", # 标准邮箱
"user.name@domain.co", # 带点的邮箱
"unicode@例子.中国" # 国际化邮箱
])
def test_valid_emails(self, user_system, email): # 测试有效邮箱格式
"""参数化测试有效邮箱格式:验证多种邮箱格式"""
user_id = user_system.add_user("Test", email) # 添加不同格式邮箱的用户
assert user_id > 1 # 验证用户创建成功
# ====================
# 修复后的执行部分
# ====================
if __name__ == "__main__": # 主程序入口
import subprocess # 导入子进程模块,用于执行外部命令
import os # 导入操作系统接口模块
import sys # 导入系统模块
# 获取当前文件所在目录的绝对路径
current_dir = os.path.dirname(os.path.abspath(__file__))
# 构建正确的pytest命令列表
command = [
"pytest", # pytest主命令
__file__, # 当前文件路径
"-v", # 详细输出模式
f"--html={os.path.join(current_dir, 'test_report.html')}", # HTML报告输出路径
f"--cov={current_dir}", # 指定覆盖率测量范围(当前目录)
"--cov-report=html" # 生成HTML格式的覆盖率报告
]
# 添加Mac系统特定优化
if sys.platform == "darwin": # 检查是否为macOS系统
command.append("--durations=10") # 显示最慢的10个测试
command.append("--color=yes") # 启用彩色输出
# 打印执行信息
print("=" * 50) # 分隔线
print("执行测试命令:") # 标题
print(" ".join(command)) # 打印完整命令
print("=" * 50) # 分隔线
try:
# 执行测试命令
result = subprocess.run(command, check=True) # 运行pytest命令
# 构建报告文件路径
report_path = os.path.join(current_dir, "test_report.html") # HTML报告路径
cov_path = os.path.join(current_dir, "htmlcov", "index.html") # 覆盖率报告路径
# 打印报告信息
print("\n" + "=" * 50) # 分隔线
print("测试报告已生成:") # 标题
print(f"• 测试报告: file://{report_path}") # 测试报告路径
print(f"• 覆盖率报告: file://{cov_path}") # 覆盖率报告路径
print("=" * 50) # 分隔线
# 退出程序,返回pytest的退出码
sys.exit(result.returncode)
except subprocess.CalledProcessError as e: # 处理命令执行错误
print(f"测试执行失败,退出码: {e.returncode}") # 打印错误信息
print("请检查是否安装了必要插件: pip install pytest pytest-html pytest-cov") # 提示安装插件
sys.exit(e.returncode) # 返回错误码
except FileNotFoundError: # 处理pytest命令未找到错误
print("错误:未找到pytest命令") # 打印错误信息
print("请确保已安装pytest:pip install pytest pytest-html pytest-cov") # 提示安装
sys.exit(1) # 返回错误码
这是我给0基础同学讲的pytest课,我觉得不太好,重新设计下