有没有办法在不使用 __init__ 的情况下在实例初始化时自动运行方法?
Posted
技术标签:
【中文标题】有没有办法在不使用 __init__ 的情况下在实例初始化时自动运行方法?【英文标题】:Is there a way to run a method automatically on the initialization of an instance without using __init__? 【发布时间】:2019-11-11 06:43:03 【问题描述】:我正在用 Pytest 编写一些单元测试。如果我希望它们被自动收集,我必须避免使用__init__
构造函数。 (如果有办法让 Pytest 使用 __init__
构造函数收集测试,我会将其作为另一个有用的答案。)
我的单元测试有一些共同的变量和方法。现在我有基础测试类TestFoo,子测试类TestBar(TestFoo)和孙测试类TestBaz(TestBar)。因为我不能有一个 init 方法,所以现在我正在调用一个 setup() 方法,它将一堆变量分配给类实例,作为每个测试方法的一部分。
看起来像:
Class TestBaz(TestBar):
def setup():
super().setup()
# do some other stuff
def test_that_my_program_works(self):
self.setup()
my_program_works = do_stuff()
assert my_program_works
但这很丑陋,我想知道是否有办法绕过它。我开始做的一件事——我制作了这个装饰器函数来装饰每个方法:
def setup(cls):
def inner_function(func):
@wraps(func)
def wrapper(*args, **kwargs):
cls.set_up()
return func(*args, **kwargs)
return wrapper
return inner_function
但是
@setup
def test_that_my_program_works():
并没有那么好。当我意识到从根本上说我不想或不需要包装每个方法时,我有点在阅读有关元类的杂草并试图弄清楚如何更安静地包装每个方法。我只想要一个在类初始化时自动执行的方法。我想要__init__
没有__init__
。
有没有办法做到这一点?
【问题讨论】:
def test_that_my_program_works():
实际上是def test_that_my_program_works(self):
,在课堂内吗?如果是这样,请将方法大写为setUp
。 Pytest 将使用它。 docs.python.org/3/library/unittest.html#unittest.TestCase.setUp
是的,抱歉,正在编辑
@Ry- 我不知道 Pytest 也使用了 setUp()。你是说我应该能够只使用 setUp() 而无需在我的方法中一遍又一遍地调用 self.setUp() 。现在这似乎对我不起作用。我收到一个TestBaz object has no attribute 'qux'
类型错误,指的是我在 setUp() 中设置的属性。也许我应该仔细阅读这个页面:docs.pytest.org/en/latest/xunit_setup.html
__init_subclass__()
是在 Python 3.6 中添加的,它可能会有所帮助(并避免创建元类的需要)。
@Katie:我认为只要你从 unittest.TestCase
继承它就应该可以工作,是的。
【参考方案1】:
夹具
您也可以将 autouse 固定装置用于方法级别的设置/拆卸。我更喜欢使用夹具,因为它们具有灵活性——如果/需要时,您可以定义特定于类的方法设置/拆卸(针对每个测试方法运行)或特定于方法的设置/拆卸(仅针对特定测试运行)。例子:
import pytest
class TestFoo:
@pytest.fixture(autouse=True)
def foo(self):
print('\nTestFoo instance setting up')
yield
print('TestFoo instance tearing down')
class TestBar(TestFoo):
@pytest.fixture(autouse=True)
def bar(self, foo):
print('TestBar instance setting up')
yield
print('TestBar instance tearing down')
class TestBaz(TestBar):
@pytest.fixture(autouse=True)
def baz(self, bar):
print('TestBaz instance setting up')
yield
print('\nTestBaz instance tearing down')
def test_eggs(self):
assert True
def test_bacon(self):
assert True
测试执行产生:
collected 2 items
test_spam.py::TestBaz::test_eggs
TestFoo instance setting up
TestBar instance setting up
TestBaz instance setting up
PASSED
TestBaz instance tearing down
TestBar instance tearing down
TestFoo instance tearing down
test_spam.py::TestBaz::test_bacon
TestFoo instance setting up
TestBar instance setting up
TestBaz instance setting up
PASSED
TestBaz instance tearing down
TestBar instance tearing down
TestFoo instance tearing down
请注意,我通过 arg 依赖项指定了夹具执行顺序(例如,def bar(self, foo):
所以bar
在foo
之后执行);如果省略参数,则不保证执行顺序foo -> bar -> baz
。如果您不需要显式排序,您可以放心地省略fixture args。
以上示例,扩展了仅针对TestBaz::test_bacon
的设置/拆卸:
class TestBaz(TestBar):
@pytest.fixture(autouse=True)
def baz(self, bar):
print('TestBaz instance setting up')
yield
print('\nTestBaz instance tearing down')
@pytest.fixture
def bacon_specific(self):
print('bacon specific test setup')
yield
print('\nbacon specific teardown')
def test_eggs(self):
assert True
@pytest.mark.usefixtures('bacon_specific')
def test_bacon(self):
assert True
执行收益率:
...
test_spam.py::TestBaz::test_bacon
TestFoo instance setting up
TestBar instance setting up
TestBaz instance setting up
bacon specific test setup
PASSED
bacon specific teardown
TestBaz instance tearing down
TestBar instance tearing down
TestFoo instance tearing down
通过将夹具范围调整为class
来实现每个类的一次性设置/拆卸:
class TestFoo:
@pytest.fixture(autouse=True, scope='class')
def foo(self):
print('\nTestFoo instance setting up')
yield
print('TestFoo instance tearing down')
class TestBar(TestFoo):
@pytest.fixture(autouse=True, scope='class')
def bar(self, foo):
print('TestBar instance setting up')
yield
print('TestBar instance tearing down')
class TestBaz(TestBar):
@pytest.fixture(autouse=True, scope='class')
def baz(self, bar):
print('TestBaz instance setting up')
yield
print('\nTestBaz instance tearing down')
def test_eggs(self):
assert True
def test_bacon(self):
assert True
执行:
collected 2 items
test_spam2.py::TestBaz::test_eggs
TestFoo instance setting up
TestBar instance setting up
TestBaz instance setting up
PASSED
test_spam2.py::TestBaz::test_bacon PASSED
TestBaz instance tearing down
TestBar instance tearing down
TestFoo instance tearing down
xUnit 方法设置/拆卸
您可以使用 xUnit 样式的设置,尤其是 Method and function level setup/teardown;这些是常用的类方法并支持继承。示例:
class TestFoo:
def setup_method(self):
print('\nTestFoo::setup_method called')
def teardown_method(self):
print('TestFoo::teardown_method called')
class TestBar(TestFoo):
def setup_method(self):
super().setup_method()
print('TestBar::setup_method called')
def teardown_method(self):
print('TestBar::teardown_method called')
super().teardown_method()
class TestBaz(TestBar):
def setup_method(self):
super().setup_method()
print('TestBaz::setup_method called')
def teardown_method(self):
print('\nTestBaz::teardown_method called')
super().teardown_method()
def test_eggs(self):
assert True
def test_bacon(self):
assert True
测试执行产生:
collected 2 items
test_spam.py::TestBaz::test_eggs
TestFoo::setup_method called
TestBar::setup_method called
TestBaz::setup_method called
PASSED
TestBaz::teardown_method called
TestBar::teardown_method called
TestFoo::teardown_method called
test_spam.py::TestBaz::test_bacon
TestFoo::setup_method called
TestBar::setup_method called
TestBaz::setup_method called
PASSED
TestBaz::teardown_method called
TestBar::teardown_method called
TestFoo::teardown_method called
【讨论】:
【参考方案2】:如您所见,py.test 有其他方法来运行类范围方法的设置。 您可能会运行它们,因为它们保证在每个(测试)方法调用之间的正确点运行 - 因为无法控制何时 py.test 实例化这样一个类。
为了记录,只需在类中添加一个setup
方法(方法名称全小写),如:
class Test1:
def setup(self):
self.a = 1
def test_blah(self):
assert self.a == 1
但是,正如您询问元类一样,是的,元类可以创建一个“等效于__init__
的自定义方法”。
当一个新对象被创建时,也就是说,当类在 Python 中被实例化时,就好像类本身被调用了一样。内部发生的是调用元类的__call__
方法,并传递参数以创建实例。
该方法然后运行类的__new__
和__init__
方法传递这些参数,并返回__new__
返回的值。
从type
继承的元类可以覆盖__call__
以添加额外的__init__
类调用,其代码如下:
class Meta(type):
def __call__(cls, *args, **kw):
instance = super().__call__(*args, **kw)
custom_init = getattr(instance, "__custom_init__", None)
if callable(custom_init):
custom_init(*args, **kw)
return instance
我已经在我用 pytest 运行的文件中尝试了一个小类,它可以正常工作:
class Test2(metaclass=Meta):
def __custom_init__(self):
self.a = 1
def test_blah(self):
assert self.a == 1
【讨论】:
以上是关于有没有办法在不使用 __init__ 的情况下在实例初始化时自动运行方法?的主要内容,如果未能解决你的问题,请参考以下文章
如何在不使用 _id 字段但使用多个属性的情况下在 mongoose 中查找子文档
加载 __init__.py 时,有没有办法自动导入我文件夹中的所有模型?