Есть ли способ запустить метод автоматически при инициализации экземпляра без использования __init__? - PullRequest
2 голосов
/ 30 июня 2019

Я пишу некоторые юнит-тесты с 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__.

Есть ли способ сделать это?

Ответы [ 2 ]

3 голосов
/ 30 июня 2019

Светильники

Вы можете использовать автономные приборы также для настройки / демонтажа на уровне метода. Я бы предпочел использовать приборы из-за их гибкости - вы можете определить настройку / разбор метода для конкретного класса (для каждого метода тестирования) или настройку / разбор для метода (только для специального теста), если / когда это необходимо. Примеры:

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 не гарантируется. Если вам не нужен явный порядок, вы можете спокойно опустить аргументы прибора.

Приведенный выше пример дополнен настройкой / разбором, характерной только для 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, в частности Установка и удаление метода и уровня функций ; это обычные методы класса и поддерживают наследование. Пример:

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 голос
/ 30 июня 2019

Как вам показалось, у 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
...