有没有办法在不使用 __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): 所以barfoo 之后执行);如果省略参数,则不保证执行顺序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 中查找子文档

如何在不使用 segue 的情况下在视图控制器之间传递图像

加载 __init__.py 时,有没有办法自动导入我文件夹中的所有模型?

深入理解 Python 中的 __init_subclass__

有没有办法在没有插件的情况下在结帐完成之前上传图片?

动态定义类的 __init__ 参数(使用先前定义的字典)