2025, Oct 30 17:17

Как расширять фикстуры Pytest в наследуемых классах без рекурсии

Как расширять фикстуры Pytest при наследовании классов без рекурсивных зависимостей: разбор проблемы и надёжное решение через алиас базовой фикстуры.

Расширять фикстуры Pytest приходится довольно часто: не всегда нужно переопределять фикстуру — иногда хочется использовать исходную и добавить к ней свои данные. В простом сценарии это работает, но ломается при наследовании классов, когда фикстура в подклассе пытается вызвать одноимённую фикстуру из базового класса. Итог — ошибка из‑за рекурсивной зависимости. Ниже — практический разбор проблемы и минимальный, надёжный обходной путь.

Повторное использование фикстуры в пределах одного класса

Простой пример частичного переиспользования фикстуры в области класса выглядит так. Фикстура возвращает базовый список, а другая фикстура с тем же именем принимает его и дополняет новыми значениями. Тест, в итоге, получает объединённые данные:

import pytest

@pytest.fixture
def numbers():
    return [1, 2, 3]

class Suite:
    @pytest.fixture
    def numbers(self, numbers):
        return numbers + [4, 5, 6]

    def test_numbers(self, numbers):
        assert 1 in numbers
        assert 2 in numbers
        assert 3 in numbers
        assert 4 in numbers
        assert 5 in numbers
        assert 6 in numbers

Этот приём работает, потому что внутренняя фикстура зависит от внешней, объявленной на уровне модуля; при разрешении имени неоднозначности нет.

Где всё рушится: наследование классов и рекурсивная зависимость

Теперь представьте, что вы расширяете фикстуру уровня класса, вызывая её базовый вариант в наследнике. Намерение то же самое, но разрешение имён работает иначе, и вы попадаете в ловушку рекурсии:

import pytest

class ParentSuite:
    @pytest.fixture
    def numbers(self):
        return [1, 2, 3]

class ChildSuite(ParentSuite):
    @pytest.fixture
    def numbers(self, numbers):
        return numbers + [4, 5, 6]

    def test_numbers(self, numbers):
        assert 1 in numbers
        assert 2 in numbers
        assert 3 in numbers
        assert 4 in numbers
        assert 5 in numbers
        assert 6 in numbers

Здесь Pytest сообщает:

E recursive dependency involving fixture 'values' detected

Второе определение фикстуры не «видит» родительскую под тем же именем. В Python нет перегрузки функций, и внутри подкласса имя разрешается к самой локальной фикстуре, которая в итоге требует саму себя — так и возникает рекурсия.

Почему простое переименование базовой фикстуры не спасает

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

import pytest

class ParentSuite:
    @pytest.fixture
    def numbers(self):
        return [1, 2, 3]

    def test_contains(numbers, item):
        assert item in numbers

class SuiteAlpha(ParentSuite):
    @pytest.fixture
    def numbers(self, numbers):
        return numbers + [4, 5, 6]

    @pytest.fixture(params=[1, 2, 3, 4, 5, 6])
    def item(self):
        return request.param

class SuiteBeta(ParentSuite):
    @pytest.fixture
    def numbers(self, numbers):
        return numbers + [7, 8, 9]

    @pytest.fixture(params=[1, 2, 3, 7, 8, 9])
    def item(self):
        return request.param

Общий тест опирается на имя фикстуры numbers. Если переименовать базовую фикстуру, придётся либо переписывать все общие тесты, либо повсеместно вводить прослойки — а это противоречит идее чистого повторного использования.

Рабочее решение: создать алиас на базовый метод и зависеть от него

Надёжный подход — создать в подклассе алиас на фикстуру базового класса и уже от него строить зависимость. Так публичное имя фикстуры остаётся прежним, а подкласс может сложить свои данные поверх базовых:

import pytest

class ParentSuite:
    @pytest.fixture
    def numbers(self):
        return [1, 2, 3]

class ChildSuite(ParentSuite):
    __seed_numbers = ParentSuite.numbers

    @pytest.fixture
    def numbers(self, __seed_numbers):
        return __seed_numbers + [4, 5, 6]

    def test_numbers(self, numbers):
        assert 1 in numbers
        assert 2 in numbers
        assert 3 in numbers
        assert 4 in numbers
        assert 5 in numbers
        assert 6 in numbers

Такой алиас сохраняет действие декораторов, и Pytest строит зависимость фикстуры подкласса от фикстуры базового класса. Подкласс по‑прежнему предоставляет tests фикстуру numbers, но внутри опирается на заалиасенную базовую реализацию.

Зачем это нужно

Тестовые наборы часто используют наследование для разделения общего поведения, и фикстуры — часть этого контракта. Если переиспользуемый тест ссылается на имя фикстуры, подклассы должны иметь возможность уточнить её, не переписывая тесты и не вводя несколько имён для одного и того же понятия. Избежание рекурсивных зависимостей важно, чтобы композиция тестов оставалась предсказуемой и без хрупких костылей, разрастающихся по коду.

Итоги

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

Статья основана на вопросе с StackOverflow от Dmitry Kuzminov и ответе Dmitry Kuzminov.