有没有一种方法可以在实例初始化时自动使用而不使用__init__的方法?

时间:2019-06-30 00:03:17

标签: python pytest decorator python-decorators metaclass

我正在用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__的{​​{1}}。

有没有办法做到这一点?

2 个答案:

答案 0 :(得分:3)

固定装置

您也可以将自动使用的夹具用于方法级的设置/拆卸。我更喜欢使用夹具,因为它们具有灵活性-您可以在需要时定义特定于类的方法设置/拆卸(针对每个测试方法运行)或定义特定于方法的设置/拆卸(仅针对特殊测试运行)。例子:

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。如果不需要显式排序,则可以安全地省略灯具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

答案 1 :(得分:1)

如您所见,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