当前位置: 首页 > news >正文

pytest 中 fixture 与类继承交互导致的问题

文章目录

      • 问题
      • 分析
        • 将属性绑定到 **类** 上
        • 使用 `scope='function'`
      • 解决方法
      • 为什么有两个不同的对象
        • 核心原因:fixture 的执行上下文
          • `scope='function'` 的情况
          • `scope='class'` 的情况
        • 为什么 pytest 要这样做?
        • 这是 pytest 的设计局限
      • 总结

本文探讨 Pytest 中 fixture 作用域与类继承的交互问题,介绍其执行顺序规则。
以 TestBase 类和 TestDerived 子类为例,指出当 init 函数的 fixture 作用域设为 class 时会出现子类无法使用 self.base 的情况,原因是 fixture 和测试方法在不同实例对象上执行。

在 pytest 中,通常情况下,fixture 的执行顺序主要由 scope 决定,但并非简单地"高级别先执行"。实际上,pytest 按照一种"由外到内"的方式执行不同 scope 的 fixture。

具体来说,fixture 执行顺序遵循以下规则:

  1. 首先按照 scope 从大到小的顺序执行:session > package > module > class > function
  2. 同一 scope 级别的 fixture 按照依赖关系执行:如果一个 fixture 依赖于另一个 fixture(通过参数引用),则先执行被依赖的 fixture
  3. 同一 scope 级别且无依赖关系的 fixture 按照它们在代码中的声明顺序执行

问题

下面是 pytest 中 fixture 作用域(scope)与 Python 类继承之间的交互方式导致的一个问题。

这个代码 TestBase 中,如果将 init 函数使用级别为 function 的scope 运行没问题,但是改成 class 级别后,子类中的方法就没使用 self.base 了。

import pytestclass TestBase:# @pytest.fixture(scope='class', autouse=True)  # 在 test_derived 中无法使用 self.base@pytest.fixture(scope='function', autouse=True)  # 可行def init(self):self.base = "base"class TestDerived(TestBase):def test_derived(self):assert self.base == "base"def test_derived2(self):assert self.base == "base"

分析

fixture 的触发机制问题

  • 对于 autouse=True 的 fixture,pytest 需要确定何时以及在哪个对象上执行它
  • 当 fixture 定义在类内部且使用 scope='class' 时,pytest 可能在处理 fixture 的执行上下文时出现了问题
将属性绑定到
import pytestclass TestBase:@pytest.fixture(scope='class', autouse=True)def init(self, request):print(f"Init fixture executing, self is: {self}")print(f"Request.cls is: {request.cls}")request.cls.base = "base"  # 确保设置在类上而不是实例上class TestDerived(TestBase):def test_derived(self):print(f"In test_derived, self is: {self}")print(f"self.__class__.base is: {getattr(self.__class__, 'base', 'NOT_FOUND')}")assert hasattr(self.__class__, 'base')assert self.__class__.base == "base"assert self.base == "base"

执行结果:

============================== 1 passed in 0.10s ==============================
Init fixture executing, self is: <src.practice_demo.te.TestDerived object at 0x0000026EA6DE32E0>
Request.cls is: <class 'src.practice_demo.te.TestDerived'>
PASSED                                  [100%]
In test_derived, self is: <src.practice_demo.te.TestDerived object at 0x0000026EA6E50760>
self.__class__.base is: base

可以发现:

  1. fixture 确实执行了Init fixture executing 说明 scope='class' 的 fixture 被正确触发
  2. 执行顺序没问题:fixture 先执行,然后才是测试方法
  3. 对象实例不同:注意两个关键的内存地址
    • fixture 中的 self: 0x0000026EA6DE32E0
    • 测试方法中的 self: 0x0000026EA6E50760

这就解释了为什么原始代码会失败,当使用类内部定义的 scope='class' fixture 时:

  • fixture 在一个 TestDerived 实例上执行(地址 2E0),设置了 self.base = "base"
  • 但测试方法 test_derived 在另一个不同的 TestDerived 实例上执行(地址 760
  • 这两个是完全不同的对象实例

解决:使用 request.cls.base = "base" 将属性设置在上而不是实例上,所以无论哪个实例都能访问到这个类属性。

使用 scope='function'

因为 function 级别的 fixture 会在每个测试方法的同一个实例上执行,所以 self.base 设置和访问都在同一个对象上。

class TestBase:@pytest.fixture(scope='function', autouse=True)def init(self):print(f"Init fixture executing, self is: {self}")self.base = 'base'class TestDerived(TestBase):def test_derived(self):print(f"In test_derived, self is: {self}")assert self.base == "base"

运行结果,地址相同:

============================== 1 passed in 0.10s ==============================
Init fixture executing, self is: <src.practice_demo.t.TestDerived object at 0x00000238DC562A90>
PASSED                                   [100%]
In test_derived, self is: <src.practice_demo.t.TestDerived object at 0x00000238DC562A90>

解决方法

使用类属性代替实例属性

如果 base 是类级别的共享状态,可以将其设置为类属性,而不是实例属性:

class TestBase:@pytest.fixture(scope='class', autouse=True)def init(self, request):request.cls.base = "base"  # 设置类属性class TestDerived(TestBase):def test_derived(self):assert self.base == "base"def test_derived2(self):assert self.base == "base"
  • 在这里,request.cls 指向当前测试类(TestDerived),通过 request.cls.base 设置类属性。
  • 这样,base 成为 TestDerived 的类属性,所有的实例都可以通过 self.base 访问。

或者,保持使用 function 级别,这确实更符合 Python 类实例的工作方式,因为每个测试方法实际上都是在一个新的类实例上运行的。

为什么有两个不同的对象

使用 scope='function'情况下,因为 function 级别的 fixture 会在每个测试方法的同一个实例上执行,所以 self.base 设置和访问都在同一个对象上。

但是,为啥 scope 为 class 时,会出现两个对象呢?

这与 pytest 的 fixture 执行机制Python 类方法调用机制 有关。

核心原因:fixture 的执行上下文

当在类内部定义 fixture 时,pytest 需要在某个对象实例上调用这个 fixture 方法。但是:

scope='function' 的情况
  1. pytest 为每个测试方法创建一个新的 TestDerived 实例
  2. 在这个实例上调用 init fixture
  3. 然后在同一个实例上调用测试方法
  4. 流程:创建实例 → 调用 fixture → 调用测试方法(都在同一个对象上)
scope='class' 的情况
  1. pytest 需要在类级别执行 fixture,但 fixture 仍然是一个实例方法
  2. pytest 创建一个 TestDerived 实例来调用 init fixture
  3. 但当执行具体的测试方法时,pytest 又创建了另一个新的实例
  4. 流程:创建实例A → 调用 fixture → 创建实例B → 调用测试方法
为什么 pytest 要这样做?

这实际上是 pytest 设计的一个特点(或者说是限制),打印对象 id :

import pytestclass TestBase:@pytest.fixture(scope='class', autouse=True)def init(self):print(f"\nFixture executing on instance: {id(self)}")self.base = "base"class TestDerived(TestBase):def test_derived(self):print(f"\nTest executing on instance: {id(self)}")print(f"hasattr(self, 'base'): {hasattr(self, 'base')}")if hasattr(self, 'base'):print(f"self.base: {self.base}")else:print("self.base does not exist")# assert hasattr(self, 'base')  # 这行会失败,先注释掉

运行这个代码:

Fixture executing on instance: 1523376596880
PASSED                                  [100%]
Test executing on instance: 1523376597312
hasattr(self, 'base'): False
self.base does not exist
这是 pytest 的设计局限

pytest 在处理类内部定义的 scope='class' fixture 时,无法很好地协调实例的生命周期。这就是为什么通常建议:

  1. 避免在类内部定义 class 级别的 fixture
  2. 将 class 级别的 fixture 定义在 conftest.py 中
  3. 或者使用 request.cls 来操作类属性而不是实例属性

所以看到的"两个对象"现象是 pytest 内部机制导致的,而不是 Python 或测试逻辑的问题。这也解释了为什么这种用法容易出现意想不到的行为。

总结

  • scope='function' 有效是因为 init 方法在每个测试函数运行时都会为当前实例设置 self.base
  • scope='class' 失败是因为 init 方法的 self 没有正确绑定到 TestDerived 的实例上,导致 self.base 未被设置。
  • 通过调整为类属性,可以解决这个问题。

相关文章:

  • pytest 常见问题解答 (FAQ)
  • python批量解析提取word内容到excel
  • 【C++高级主题】命令空间(五):类、命名空间和作用域
  • 【Day41】
  • 零基础一站式端游内存辅助编写教程(无密)
  • ShenNiusModularity项目源码学习(32:ShenNius.Admin.Mvc项目分析-17)
  • UVa1457/LA4746 Decrypt Messages
  • python里面导入yfinance的时候报错
  • 小白的进阶之路系列之八----人工智能从初步到精通pytorch综合运用的讲解第一部分
  • tomcat yum安装
  • day07
  • C++面试5——对象存储区域详解
  • IDM下载器 Internet Download Manager v6.42 Build 39
  • 深入理解设计模式之访问者模式
  • leetcode hot100刷题日记——34.将有序数组转换为二叉搜索树
  • 力扣HOT100之动态规划:152. 乘积最大子数组
  • C#数字图像处理(一)
  • 2、PyTorch基础教程:从张量到神经网络训练
  • FactoryBean 接口
  • 【HW系列】—溯源与定位—Linux入侵排查
  • 网站建设公司优惠大酬宾活动/百度竞价推广公司
  • 铜梁网站建设/网络运营工作内容
  • 做教程网站如何查用户搜索/seo关键词外包
  • 免费域名网站推荐/哪里做网络推广
  • 差异基因做聚类分析网站/世界足球世界排名
  • 百度网站怎么做视频/seo排名优化北京