Сообщить об ошибке.

Декоратор mark.usefixtures и autouse-фикстуры модуля pytest в Python

Неявно вызываемые и автоматические фикстуры модуля pytest

В материале рассматривается как неявно вызвать фикстуру декоратором @pytest.mark.usefixtures, от работы которой зависит прохождение теста, а так-же автоматически вызываемые фикстуры фреймворка pytest.

Содержание:


Неявный вызов фикстур декоратором @pytest.mark.usefixtures.

Бывают ситуации, когда тестовым функциям не требуется прямой доступ к объекту фикстуры и при этом необходимо, чтобы она запускалась в нужное время. Представьте ситуацию, что для работы тестов необходим пустая папка, которую они будут использовать в качестве рабочего каталога. Что бы решить такую задачу, можно использовать стандартный модуль tempfile и обычную фикстуру pytest, которая создает пустой каталог. Что бы заставить запускаться такую фикстуру с определенным тестом, можно объявить ее использование с помощью маркера @pytest.mark.usefixtures перед нужным тестом.

Создадим фикстуру в файл conftest.py:

# conftest.py
import os
import shutil
import tempfile
import pytest

@pytest.fixture
def cleandir():
    old_cwd = os.getcwd()
    newpath = tempfile.mkdtemp()
    os.chdir(newpath)
    yield
    # очистка директорий после
    # завершения теста
    os.chdir(old_cwd)
    shutil.rmtree(newpath)

Далее объявим использование этой фикстуры в тестовом модуле с помощью маркера @pytest.mark.usefixtures:

# test_setenv.py
import os
import pytest


# объявим использование фикстуры
@pytest.mark.usefixtures("cleandir")
class TestDirectoryInit:

    def test_cwd_starts_empty(self):
        assert os.listdir(os.getcwd()) == []
        with open("myfile", "w") as f:
            f.write("hello")

    def test_cwd_again_starts_empty(self):
        assert os.listdir(os.getcwd()) == []

Декоратор @pytest.mark.usefixtures заставляет фикстуру cleandir() инициализироваться для выполнения с каждым тестовым методом класса.

Декоратор @pytest.mark.usefixtures поддерживает указание несколько фикстур следующим образом:

@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test():
    ...

Если определить переменную pytestmark в модуле с тестами, то можно заставить работать такую фикстуры на уровне модуля (т.е. будет выполняться для всех тестов модуля):

pytestmark = pytest.mark.usefixtures("cleandir")

Обратите внимание, что переменная должна называться именно pytestmark; если назвать ее по другому, то фикстура инициализироваться не будет.

Можно также заставить фикстуру выполняться для всех тестов проекта, указав в ее ini-файле:

# pytest.ini
[pytest]
usefixtures = cleandir

Автоматически вызываемые фикстуры.

Иногда может понадобиться фикстура (или даже несколько), от которой, например, зависят все тесты (т.е. фикстура должна неявно вызываться всеми тестами). Фикстуры, с указанным аргументом autouse=True - это удобный способ сделать так, чтобы все тесты, расположенные в области действия фикстуры автоматически запрашивали ее. Такое поведение может исключить множество избыточных запросов и даже может обеспечить более расширенное использование фикстуры.

Аutouse-фикстуры соблюдают область действия, определенную с помощью аргумента scope!

Предположим, есть фикстура, имитирующая базу данных с архитектурой "begin/rollback/commit" и нужно автоматически обернуть каждый тестовый метод транзакцией и откатом к начальному состоянию.

#test_db_transact.py
import pytest

class DB:
    def __init__(self):
        self.intransaction = []

    def begin(self, name):
        self.intransaction.append(name)

    def rollback(self):
        self.intransaction.pop()


@pytest.fixture(scope="module")
def db():
    return DB()

class TestClass:
    @pytest.fixture(autouse=True)
    def transact(self, request, db):
        db.begin(request.function.__name__)
        yield
        db.rollback()

    def test_method1(self, db):
        assert db.intransaction == ["test_method1"]

    def test_method2(self, db):
        assert db.intransaction == ["test_method2"]

Аutouse-фикстура transact() уровня класса, это означает, что все тестовые методы класса будут использовать ее без необходимости указывать ее в сигнатуре тестовой функции или применять декоратор @pytest.mark.usefixtures к классу TestClass.

Фикстуру transact() можно сделать доступной для всего проекта, не будучи при этом активной. Классический способ сделать это - поместить ее в файл conftest.py, расположенный на уровне проекта и не указывать аргумент autouse=True:

# conftest.py
@pytest.fixture
def transact(request, db):
    db.begin()
    yield
    db.rollback()

И затем, если фикстура понадобится, создать тестовый класс, и объявить ее использование при помощи декоратора @pytest.mark.usefixtures():

@pytest.mark.usefixtures("transact")
class TestClass:
    def test_method1(self):
        ...

Предосчторожности и особенности использования аutouse-фикстур.

Автоматически вызываемые фикстуры (простые фикстуры с включенным аргументом autouse=True) выполняются первыми в пределах их области их действия.

Будьте осторожны с аutouse-фикстурами, т.к. они будет автоматически выполняться для каждого теста, даже если он этого не запрашивает.

Автоматически вызываемые фикстуры (далее для краткости аutouse-фикстуры) применяются к каждому тесту, следовательно они выполняются перед другими фикстурами в этой области. Фикстуры, запрашиваемые аutouse-фикстурами, фактически сами становятся автоматически используемыми фикстурами для тестов, к которым применяется настоящая аutouse-фикстура.

Таким образом, если a - это аutouse-фикстура, а фикстура b - нет, при этом фикстура a запрашивает фикстуру b, то фикстура b также будет автоматически используемой фикстурой, но только для тестов, которые запрашивают фикстуру a.

import pytest

@pytest.fixture(scope="class")
def order():
    return []

@pytest.fixture(scope="class", autouse=True)
def c1(order):
    order.append("c1")

@pytest.fixture(scope="class")
def c2(order):
    order.append("c2")

@pytest.fixture(scope="class")
def c3(order, c1):
    order.append("c3")

class TestClassWithC1Request:
    def test_order(self, order, c1, c3):
        assert order == ["c1", "c3"]

class TestClassWithoutC1Request:
    def test_order(self, order, c2):
        assert order == ["c1", "c2"]

Несмотря на то, что в классе TestClassWithoutC1Request() ничего не запрашивает фикстуру c1(), она все равно выполняется для тестов внутри него. Но только потому, что одна аutouse-фикстура запросила фикстуру, не предназначенную для автоматического использования. Это не означает, что фикстура, не предназначенная для автоматического использования, становится фикстурой автоматического использования для всех контекстов, к которым она может применяться. Она эффективно становится аutouse-фикстура только для контекстов, к которым может применяться настоящая аutouse-фикстура (та, которая запросила фикстуру, не предназначенную для автоматического использования).