【pytest官方文档】解读fixtures - 8. yield和addfinalizer的区别(填坑)

本文讲述了作者在使用pytest时遇到的问题:在fixture中故意引发错误,但预期的finalizer代码未执行。通过深入理解 pytest 的行为和修正代码顺序,揭示了fixture执行与finalizer关联的关键。

上一章中,文末留下了一个坑待填补,疑问是这样的:

目前从官方文档中看到的是

We have to be careful though, because pytest will run that finalizer once it’s been added, 
even if that fixture raises an exception after adding the finalizer.

一旦添加了终结器,pytest便会执行。

但是,当我尝试在setup代码中进行抛错,终结器的代码却并没有执行。
尝试搜索外网暂时也没得到有效的帮助,只能在GitHub上向pytest提了issue了,这里算是埋下一个坑,待后续解决。

一、问题回顾

其实说到底还是我理解的不对,可能当时自己处在疑问中难免就会陷入进死循环,后来在github上经过别人提点方才醒悟。
先来看下当时我尝试演示出上述结果的代码,也就是:setup代码中进行抛错,终结器的代码却并没有执行。

代码分为2部分,一个是fixture函数代码,另一个则是测试用例。代码是不能直接copy出来运行的,是我在项目的用例中
进行改造的,在这里仅仅帮助说明意思。

# content of conftest.py

@pytest.fixture()
def init_data_allot_task(request):
    query_sql = """
    SELECT id FROM `sm_purchase_allot` WHERE `status`!=5
    """
    db = DB()
    data = db.fetch_one(query_sql)
    db.close()

    def demo_finalizer():
        print("running finalizer code...")
    request.addfinalizer(demo_finalizer)
    return data
# content of testcase
...
def test_allot_detail(init_data_allot_task):
    """

    """
    payload = {
          "allotId": init_data_allot_task[0]
        }
    r = requests.post(QA_URL + API_URL, json=payload, headers=HEADER)
    result = r.json()

    assert result["result"] == "ok"
    assert result["errmsg"] == "success"
    assert len(result["row"]["taskListOfPage"]["resultData"]) > 0

最开始我想做的是,在fixture函数中,让代码db = DB()抛出一个mysql连接超时的错误,
然后就能在控制台中看到"running finalizer code..."的输出。

但是我执行后,并没有看到预期的输出,说明setup代码抛错后,addfinalizer代码并没有执行。

最后经过github上朋友指点后,发现还是我自己理解错了。

二、问题解决

还是来看下官方的原文:

We have to be careful though, because pytest will run that finalizer once it’s been added, 
even if that fixture raises an exception after adding the finalizer.

这句话意思其实是说,当finalizer 一旦添加成功后,pytest就会去执行它。就算是fixture函数在添加了finalizer之后
抛出了异常。

按照这样理解的话,那我在fixture函数中的代码就有问题了。因为db = DB()代码在request.addfinalizer(demo_finalizer)
之前就抛错了,那么实际上并没有执行到添加终结器的这行代码,所以终结器都还没添加成功,又怎么会去执行呢?

终于我明白过来了,于是调整了代码顺序,把request.addfinalizer(demo_finalizer)放到前面去,然后再接上fixture的代码:

# content of conftest.py
@pytest.fixture()
def init_data_allot_task(request):
    query_sql = """
    SELECT id FROM `sm_purchase_allot` WHERE `status`!=5 
    """
    def demo_finalizer():
        print("running finalizer code...")
    request.addfinalizer(demo_finalizer)
    print("running setup code...")

    db = DB()
    data = db.fetch_one(query_sql)
    db.close()
    return data

如此来看,我们会先看到"running setup code..."的输出,然后看到mysql抛错,
最后仍然可以看到"running setup code..."的输出。

运行代码验证一下:

这下就对了。

# -*- 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 import sys from datetime import datetime from unittest.mock import MagicMock, patch # -------------------- # 1. 测试函数与断言 # -------------------- def test_addition(): """基础断言示例""" assert 1 + 1 == 2, "加法计算错误" 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 assert result == pytest.approx(0.3, rel=1e-5) # -------------------- # 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): """函数级夹具 - 依赖其他夹具""" print("\n>> 创建临时用户") user = {"id": 1001, "name": "test_user", "created_at": datetime.now()} yield user print("\n>> 删除临时用户") def test_user_creation(temporary_user): """测试用户创建""" assert temporary_user["id"] == 1001 assert "test" in temporary_user["name"] # -------------------- # 3. 参数化高级用法 (修复版) # -------------------- def is_prime(n): """判断质数的函数""" if n < 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(0, False, id="edge_case_0"), ], ids=lambda param: f"num_{param[0]}" if isinstance(param, tuple) else param.id ) def test_prime_numbers(number, expected): """测试质数判断函数""" 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(): """测试海象运算符""" 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(): """测试文件系统大小写敏感性""" 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 return {"temp": 25, "condition": "sunny"} def test_mock_external_api(): """使用Mock替代外部API调用""" # 创建Mock替代实际函数 with patch(__name__ + ".fetch_weather_data") as mock_weather: mock_weather.return_value = {"temp": 30, "condition": "cloudy"} result = fetch_weather_data("https://weather-api.com") assert result["temp"] == 30 mock_weather.assert_called_once_with("https://weather-api.com") # -------------------- # 7. 临时文件与目录 # -------------------- def test_create_temp_files(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 assert "Bob" in content # ==================== # 第三部分:高级工程实践 # ==================== # -------------------- # 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 assert calculate_discount(50, 10) == 45 def test_discount_edge_cases(): with pytest.raises(ValueError): calculate_discount(100, -5) with pytest.raises(ValueError): calculate_discount(100, 110) # -------------------- # 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): """参数化夹具""" 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 def add_user(self, name, email): """添加用户""" if not email or "@" not in email: raise ValueError("无效的邮箱地址") user_id = self.next_id self.users[user_id] = {"name": name, "email": email} self.next_id += 1 return user_id def get_user(self, user_id): """获取用户信息""" return self.users.get(user_id) def delete_user(self, user_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 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", __file__, "-v", f"--html={os.path.join(current_dir, 'test_report.html')}", f"--cov={current_dir}", # 指定当前目录为覆盖率测量范围 "--cov-report=html" ] # 添加Mac特定优化 if sys.platform == "darwin": command.append("--durations=10") # 显示最慢的10个测试 command.append("--color=yes") # 彩色输出 print("=" * 50) print("执行测试命令:") print(" ".join(command)) print("=" * 50) try: # 执行测试 result = subprocess.run(command, check=True) # 报告路径 report_path = os.path.join(current_dir, "test_report.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) 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: print("错误:未找到pytest命令") print("请确保已安装pytest:pip install pytest pytest-html pytest-cov") sys.exit(1) 需要每一行都加上注释
08-26
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值