Как накапливать состояние по тестам в py.test - PullRequest
3 голосов
/ 30 июня 2010

У меня сейчас есть проект и тесты, подобные этим.

class mylib:
    @classmethod
    def get_a(cls):
        return 'a'

    @classmethod
    def convert_a_to_b(cls, a):
        return 'b'

    @classmethod
    def works_with(cls, a, b):
        return True

class TestMyStuff(object):
    def test_first(self):
        self.a = mylib.get_a()

    def test_conversion(self):
        self.b = mylib.convert_a_to_b(self.a)

    def test_a_works_with_b(self):
        assert mylib.works_with(self.a, self.b)

При py.test 0.9.2 эти тесты (или аналогичные) проходят успешно. В более поздних версиях py.test, test_conversion и test_a_works_with_b завершаются с ошибкой «TestMyStuff не имеет атрибута a».

Я предполагаю, что это потому, что в более поздних сборках py.test отдельный экземпляр TestMyStuff создается для каждого тестируемого метода.

Как правильно написать эти тесты так, чтобы результаты могли быть даны для каждого из шагов в последовательности, но состояние из предыдущего (успешного) теста может (должно) использоваться для выполнения последующих тестов?

Ответы [ 5 ]

8 голосов
/ 30 июня 2010

Хорошей практикой юнит-тестов является недопущение накопления состояний во время тестов.Большинство фреймворков модульных тестов идут на многое, чтобы предотвратить накопление состояния.Причина в том, что вы хотите, чтобы каждый тест стоял отдельно.Это позволяет вам запускать произвольные подмножества ваших тестов и гарантирует, что ваша система находится в чистом состоянии для каждого теста.

7 голосов
/ 15 июля 2010

Я частично согласен с Недом в том, что хорошо избегать случайного совместного использования тестового состояния. Но я также думаю, что иногда полезно накапливать состояние постепенно во время тестов.

С py.test вы можете сделать это, явно указав, что хотите поделиться тестовым состоянием. Ваш пример переписан для работы:

class State:
    """ holding (incremental) test state """

def pytest_funcarg__state(request):
    return request.cached_setup(
        setup=lambda: State(),
        scope="module"
    )

class mylib:
    @classmethod
    def get_a(cls):
        return 'a'

    @classmethod
    def convert_a_to_b(cls, a):
        return 'b'

    @classmethod
    def works_with(cls, a, b):
        return True

class TestMyStuff(object):
    def test_first(self, state):
        state.a = mylib.get_a()

    def test_conversion(self, state):
        state.b = mylib.convert_a_to_b(state.a)

    def test_a_works_with_b(self, state):
        mylib.works_with(state.a, state.b)

Вы можете запустить это с последними версиями py.test. Каждая функция получает объект «state», а фабрика «funcarg» создает его изначально и кэширует в области видимости модуля. Вместе с гарантией py.test, что тесты выполняются в порядке файлов, функции тестов могут быть скорее такими, что они будут работать постепенно в «состоянии» теста.

Однако, это немного хрупко, потому что, если вы выбираете только запуск "test_conversion" через, например, «py.test -k test_conversion», то ваш тест не пройден, потому что первый тест не был запущен. Я думаю, что какой-то способ сделать инкрементные тесты был бы хорош, поэтому, возможно, мы в конечном итоге сможем найти абсолютно надежное решение.

НТН, Хольгер

1 голос
/ 11 июля 2017

Это, безусловно, работа для приборов pytest: https://docs.pytest.org/en/latest/fixture.html

Светильники позволяют тестовым функциям легко получать и работать с конкретными предварительно инициализированными объектами приложения, не заботясь об импорте / настройке / очисткеподробности.Это яркий пример внедрения зависимостей, когда функции приборов принимают роль инжектора, а тестовые функции являются потребителями объектов приборов.

Таким образом, пример установки устройства в состояние удержания будет следующим::

import pytest


class State:

    def __init__(self):
        self.state = {}


@pytest.fixture(scope='session')
def state() -> State:
    state = State()
    state.state['from_fixture'] = 0
    return state


def test_1(state: State) -> None:
    state.state['from_test_1'] = 1
    assert state.state['from_fixture'] == 0
    assert state.state['from_test_1'] == 1


def test_2(state: State) -> None:
    state.state['from_test_2'] = 2
    assert state.state['from_fixture'] == 0
    assert state.state['from_test_1'] == 1
    assert state.state['from_test_2'] == 2

Обратите внимание, что вы можете указать область для внедрения зависимости (и, следовательно, состояние).В этом случае я установил его на сеанс, другой вариант будет module (scope=function не будет работать для вашего варианта использования, поскольку вы потеряете состояние между функциями.

Очевидно, вы можете расширить этошаблон для хранения других типов объектов в состоянии, таких как сравнение результатов различных тестов.

В качестве предупреждения - вы все равно хотите иметь возможность запускать свои тесты в любом порядке (мой пример нарушает этосмена порядка 1 и 2 приводит к неудаче.) Однако я не проиллюстрировал это для простоты.

0 голосов
/ 27 июля 2018

Чтобы дополнить ответ hpk42 , вы также можете использовать pytest-steps для выполнения инкрементального тестирования, это может помочь вам, в частности, если вы хотите поделиться каким-либо инкрементным состоянием / промежуточным состояниемрезультаты между шагами.

С этим пакетом вам не нужно помещать все шаги в классе (вы можете, но это не обязательно), просто украсьте вашу функцию "test suite" с помощью @test_steps.

РЕДАКТИРОВАТЬ: есть новый режим «генератор», чтобы сделать его еще проще:

from pytest_steps import test_steps

@test_steps('step_first', 'step_conversion', 'step_a_works_with_b')
def test_suite_with_shared_results():
    a = mylib.get_a()
    yield

    b = mylib.convert_a_to_b(a)
    yield

    assert mylib.works_with(a, b)
    yield

LEGACY answer:

Вы можете добавить параметр steps_data в свою тестовую функцию, если хотитеразделить объект StepsDataHolder между вашими шагами.

Ваш пример напишет:

from pytest_steps import test_steps, StepsDataHolder

def step_first(steps_data):
    steps_data.a = mylib.get_a()


def step_conversion(steps_data):
    steps_data.b = mylib.convert_a_to_b(steps_data.a)


def step_a_works_with_b(steps_data):
    assert mylib.works_with(steps_data.a, steps_data.b)


@test_steps(step_first, step_conversion, step_a_works_with_b)
def test_suite_with_shared_results(test_step, steps_data: StepsDataHolder):

    # Execute the step with access to the steps_data holder
    test_step(steps_data)

Наконец, обратите внимание, что вы можете автоматически пропустить или пропустить шаг, если другой не удался с помощью @depends_on, проверьте документацию для деталей.

(кстати, я автор этого пакета;))

0 голосов
/ 15 июля 2010

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

То, что я в итоге использовал для некоторых своих классов, где сам класс представлял собойобрабатывая это накопленное состояние, я сохранял накопленное состояние в самом объекте класса.

class mylib:
    @classmethod
    def get_a(cls):
        return 'a'

    @classmethod
    def convert_a_to_b(cls, a):
        return 'b'

    @classmethod
    def works_with(cls, a, b):
        return True

class TestMyStuff(object):
    def test_first(self):
        self.__class__.a = mylib.get_a()

    def test_conversion(self):
        self.__class__.b = mylib.convert_a_to_b(self.a)

    def test_a_works_with_b(self):
        mylib.works_with(self.a, self.b)

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

Я думаю, что каждый из этих подходов, обсуждаемых таким образом, имеет свои достоинства,и намереваемся использовать каждый подход там, где он подходит лучше всего.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...