为什么我的具有函数范围的 pytest fixture 返回一个对象,该对象没有在新测试中重置它的 class 变量?
Why is my pytest fixture with function scope returning an object that isn't resetting it's class variables in a new test?
我有一个名为 Person() 的 class。它有一个 CURRENT_YEAR class 变量,旨在在 class 的所有实例之间共享。
我希望单个模块中的每个测试都能得到一个新的(新的)对象,因为我将夹具的范围限定为 'function'。但是,当我在一个测试函数中更改 CURRENT_YEAR 时(使用更改 Person.CURRENT_YEAR 值的 class 方法发生),它会持续到下一个测试函数中。很明显,该对象不会在每次测试时被清除和重新创建。
夹具在 conftest.py 中创建,可供所有测试访问。
最后,我把它全部拆解,把东西搬来搬去,但总是看到同样的东西。 Person() class 没有像我预期的那样被多次实例化。应该如何创建夹具,以便每个 test_ 函数都有自己的 Class 范围?
我试过将测试移动到单独的模块,但没有用。
我尝试制作第二个夹具,returns 一个 Person() 对象。没有不同。
我真的在下面的代码中将它精简了,所以希望它清楚我正在尝试什么以及为什么我感到困惑。
project_root/tests/test_temp.py
import os,sys
tests = os.path.dirname(__file__)
project = os.path.dirname(tests)
sys.path.insert(0,project)
import pytest
from app.person import *
def test_current_year_changes(person_fixture):
import pytest
p = person_fixture
print(f"CY is {p.CURRENT_YEAR} and age is {p.get_age()}")
p.add_years_to_age(20)
print(f"CY is {p.CURRENT_YEAR} and age is {p.get_age()}")
assert p.CURRENT_YEAR == 20
def test_current_year_changes2(person_fixture2):
import pytest
p = person_fixture2
print(f"CY is {p.CURRENT_YEAR} and age is {p.get_age()}")
p.add_years_to_age(20)
print(f"CY is {p.CURRENT_YEAR} and age is {p.get_age()}")
assert p.CURRENT_YEAR == 20
@pytest.fixture(scope='function')
def person_fixture():
p = Person()
return p
@pytest.fixture(scope='function')
def person_fixture2():
p = Person()
return p
project_root/app/person.py
class Person(object):
CURRENT_YEAR = 0
def __init__(self, name=""):
self.name = name
self.birth_year = Person.CURRENT_YEAR
def add_years_to_age(self, years=1):
Person.CURRENT_YEAR += years
def get_age(self):
return Person.CURRENT_YEAR - self.birth_year
代码看起来两个测试应该是非常独立的。但是第二个测试函数显示 CURRENT_YEAR 不是以新的 class 变量开始的。
断言失败表明 Person.CURRENT_YEAR
是 40,而不是 20
fixture scope 只是定义了用 @pytest.fixture
装饰的函数何时是 运行。这只是将通用测试代码分解为单独函数的一种方法。
所以在你的情况下它是 "function"
所以 fixture 将为每个测试函数(使用 fixture)执行函数并创建一个 Person
实例。同样,如果范围是 "module"
.
,每个测试模块将 运行 一次
这完全符合预期。它不仅按 pytest 的预期工作,而且按您自己的预期工作 - 请记住,您实际上想在不同实例之间共享 CURRENT_YEAR
!
How should a fixture be created, so that each test_ function gets its own scope for the Class?
你真的不应该使用全局变量或静态变量(class 变量只是隐藏在 class 后面的全局变量)正是因为它使测试变得非常困难(并使程序非线程安全)。还请记住,如果您不提供,pytest 无法提供重置程序的基础设施!想一想:到底应该发生什么?它应该为每个测试函数创建一个新的解释器会话吗?它应该重新加载模块吗?它应该重新加载 class 定义吗?是否应该将 Person.CURRENT_YEAR 设置为零?
解决这个问题的一种方法是抽象 class 变量,例如使用环境 class(当前年份似乎也不适合 Person class无论如何):
class Environment(object):
def __init__(self):
self.CURRENT_YEAR = 0
class Person(object):
def __init__(self, environment, name=""):
self.environment = environment
self.name = name
self.birth_year = self.environment.CURRENT_YEAR
def add_years_to_age(self, years=1):
self.environment.CURRENT_YEAR += years
def get_age(self):
return self.environment.CURRENT_YEAR - self.birth_year
然后让 fixture 创建一个新的环境和人物实例:
@pytest.fixture(scope='function')
def person_fixture():
e = Environment()
p = Person(e)
return p
到那时,您可能需要在代码中使用全局 Environment
实例,以便不同的 Person
实例可以共享它。
请注意,如果它只是一个变量,这就没有多大意义,并且您最终可能会针对不同的环境变量得到不同的 classes。如果您的应用变得更加复杂,您可能需要考虑依赖注入来管理这种复杂性。
但是,如果您只想为每个使用 person_fixture
的函数重置 CURRENT_YEAR
,您也可以在夹具中将其设置为 0:
@pytest.fixture(scope='function')
def person_fixture_with_current_year_reset():
Person.CURRENT_YEAR = 0
p = Person()
return p
这应该暂时有效,但在您 运行 并行测试时,您可能会看到随机失败,因为全局变量(和 class 变量)本质上是非线程安全的。
我有一个名为 Person() 的 class。它有一个 CURRENT_YEAR class 变量,旨在在 class 的所有实例之间共享。
我希望单个模块中的每个测试都能得到一个新的(新的)对象,因为我将夹具的范围限定为 'function'。但是,当我在一个测试函数中更改 CURRENT_YEAR 时(使用更改 Person.CURRENT_YEAR 值的 class 方法发生),它会持续到下一个测试函数中。很明显,该对象不会在每次测试时被清除和重新创建。
夹具在 conftest.py 中创建,可供所有测试访问。
最后,我把它全部拆解,把东西搬来搬去,但总是看到同样的东西。 Person() class 没有像我预期的那样被多次实例化。应该如何创建夹具,以便每个 test_ 函数都有自己的 Class 范围?
我试过将测试移动到单独的模块,但没有用。
我尝试制作第二个夹具,returns 一个 Person() 对象。没有不同。
我真的在下面的代码中将它精简了,所以希望它清楚我正在尝试什么以及为什么我感到困惑。
project_root/tests/test_temp.py
import os,sys
tests = os.path.dirname(__file__)
project = os.path.dirname(tests)
sys.path.insert(0,project)
import pytest
from app.person import *
def test_current_year_changes(person_fixture):
import pytest
p = person_fixture
print(f"CY is {p.CURRENT_YEAR} and age is {p.get_age()}")
p.add_years_to_age(20)
print(f"CY is {p.CURRENT_YEAR} and age is {p.get_age()}")
assert p.CURRENT_YEAR == 20
def test_current_year_changes2(person_fixture2):
import pytest
p = person_fixture2
print(f"CY is {p.CURRENT_YEAR} and age is {p.get_age()}")
p.add_years_to_age(20)
print(f"CY is {p.CURRENT_YEAR} and age is {p.get_age()}")
assert p.CURRENT_YEAR == 20
@pytest.fixture(scope='function')
def person_fixture():
p = Person()
return p
@pytest.fixture(scope='function')
def person_fixture2():
p = Person()
return p
project_root/app/person.py
class Person(object):
CURRENT_YEAR = 0
def __init__(self, name=""):
self.name = name
self.birth_year = Person.CURRENT_YEAR
def add_years_to_age(self, years=1):
Person.CURRENT_YEAR += years
def get_age(self):
return Person.CURRENT_YEAR - self.birth_year
代码看起来两个测试应该是非常独立的。但是第二个测试函数显示 CURRENT_YEAR 不是以新的 class 变量开始的。
断言失败表明 Person.CURRENT_YEAR
是 40,而不是 20
fixture scope 只是定义了用 @pytest.fixture
装饰的函数何时是 运行。这只是将通用测试代码分解为单独函数的一种方法。
所以在你的情况下它是 "function"
所以 fixture 将为每个测试函数(使用 fixture)执行函数并创建一个 Person
实例。同样,如果范围是 "module"
.
这完全符合预期。它不仅按 pytest 的预期工作,而且按您自己的预期工作 - 请记住,您实际上想在不同实例之间共享 CURRENT_YEAR
!
How should a fixture be created, so that each test_ function gets its own scope for the Class?
你真的不应该使用全局变量或静态变量(class 变量只是隐藏在 class 后面的全局变量)正是因为它使测试变得非常困难(并使程序非线程安全)。还请记住,如果您不提供,pytest 无法提供重置程序的基础设施!想一想:到底应该发生什么?它应该为每个测试函数创建一个新的解释器会话吗?它应该重新加载模块吗?它应该重新加载 class 定义吗?是否应该将 Person.CURRENT_YEAR 设置为零?
解决这个问题的一种方法是抽象 class 变量,例如使用环境 class(当前年份似乎也不适合 Person class无论如何):
class Environment(object):
def __init__(self):
self.CURRENT_YEAR = 0
class Person(object):
def __init__(self, environment, name=""):
self.environment = environment
self.name = name
self.birth_year = self.environment.CURRENT_YEAR
def add_years_to_age(self, years=1):
self.environment.CURRENT_YEAR += years
def get_age(self):
return self.environment.CURRENT_YEAR - self.birth_year
然后让 fixture 创建一个新的环境和人物实例:
@pytest.fixture(scope='function')
def person_fixture():
e = Environment()
p = Person(e)
return p
到那时,您可能需要在代码中使用全局 Environment
实例,以便不同的 Person
实例可以共享它。
请注意,如果它只是一个变量,这就没有多大意义,并且您最终可能会针对不同的环境变量得到不同的 classes。如果您的应用变得更加复杂,您可能需要考虑依赖注入来管理这种复杂性。
但是,如果您只想为每个使用 person_fixture
的函数重置 CURRENT_YEAR
,您也可以在夹具中将其设置为 0:
@pytest.fixture(scope='function')
def person_fixture_with_current_year_reset():
Person.CURRENT_YEAR = 0
p = Person()
return p
这应该暂时有效,但在您 运行 并行测试时,您可能会看到随机失败,因为全局变量(和 class 变量)本质上是非线程安全的。