Как использовать тесты на основе unittest в pytest

pytest поддерживает выполнение тестов на основе Python unittest из коробки. Он предназначен для использования существующих наборов тестов на основе unittest для использования pytest в качестве программы запуска тестов, а также для постепенной адаптации набора тестов для использования всех возможностей pytest.

Чтобы запустить существующий набор тестов в стиле unittest с помощью pytest, введите:

pytest tests

pytest будет автоматически собирать unittest.TestCase подклассы и их test методы в test_*.py или *_test.py файлах.

Поддерживаются почти все функции unittest:

  • Декораторы стиля @unittest.skip;

  • setUp/tearDown;

  • setUpClass/tearDownClass;

  • setUpModule/tearDownModule;

Кроме того, subtests поддерживаются плагином pytest-subtests.

На данный момент pytest не поддерживает следующие функции:

Преимущества из коробки

Запуская свой набор тестов с помощью pytest, вы можете использовать несколько возможностей, в большинстве случаев без необходимости изменять существующий код:

возможности pytest в подклассах unittest.TestCase

Следующие функции pytest работают в подклассах unittest.TestCase:

Следующие функции pytest не работают и, вероятно, никогда не будут работать из-за различных философий проектирования:

Сторонние плагины могут работать или не работать хорошо, в зависимости от плагина и набора тестов.

Смешивание фикстур pytest в подклассы unittest.TestCase с использованием меток

Запуск вашего unittest с pytest позволяет вам использовать его fixture mechanism с тестами в стиле unittest.TestCase. Предполагая, что вы хотя бы бегло ознакомились с возможностями фикстуры pytest, давайте перейдем к примеру, который интегрирует фикстуру pytest db_class, устанавливая объект базы данных, кэшируемый классом, и затем обращаясь к нему из теста в стиле unittest:

# content of conftest.py

# we define a fixture function below and it will be "used" by
# referencing its name from tests

import pytest


@pytest.fixture(scope="class")
def db_class(request):
    class DummyDB:
        pass

    # set a class attribute on the invoking test context
    request.cls.db = DummyDB()

Это определяет функцию приспособления db_class, которая - если используется - вызывается один раз для каждого тестового класса и устанавливает атрибут db уровня класса в экземпляр DummyDB. Функция приспособления достигает этого, получая специальный объект request, который предоставляет доступ к the requesting test context, таким как атрибут cls, обозначающий класс, из которого используется приспособление. Такая архитектура отделяет написание приспособления от фактического кода тестирования и позволяет повторно использовать приспособление по минимальной ссылке - имени приспособления. Итак, давайте напишем реальный класс unittest.TestCase, используя наше определение приспособления:

# content of test_unittest_db.py

import unittest

import pytest


@pytest.mark.usefixtures("db_class")
class MyTest(unittest.TestCase):
    def test_method1(self):
        assert hasattr(self, "db")
        assert 0, self.db  # fail for demo purposes

    def test_method2(self):
        assert 0, self.db  # fail for demo purposes

Декоратор классов @pytest.mark.usefixtures("db_class") следит за тем, чтобы функция pytest fixture db_class вызывалась один раз для каждого класса. Благодаря намеренно неудачным утверждениям assert, мы можем посмотреть на значения self.db в трассировке:

$ pytest test_unittest_db.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 2 items

test_unittest_db.py FF                                               [100%]

================================= FAILURES =================================
___________________________ MyTest.test_method1 ____________________________

self = <test_unittest_db.MyTest testMethod=test_method1>

    def test_method1(self):
        assert hasattr(self, "db")
>       assert 0, self.db  # fail for demo purposes
E       AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
E       assert 0

test_unittest_db.py:11: AssertionError
___________________________ MyTest.test_method2 ____________________________

self = <test_unittest_db.MyTest testMethod=test_method2>

    def test_method2(self):
>       assert 0, self.db  # fail for demo purposes
E       AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
E       assert 0

test_unittest_db.py:14: AssertionError
========================= short test summary info ==========================
FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: <conft...
FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: <conft...
============================ 2 failed in 0.12s =============================

Эта трассировка pytest по умолчанию показывает, что два тестовых метода используют один и тот же экземпляр self.db, что и было нашим намерением при написании функции фикстуры, привязанной к классу, описанному выше.

Использование приспособлений autouse и доступ к другим приспособлениям

Хотя обычно лучше явно объявлять использование фикстур, необходимых для конкретного теста, иногда вы можете захотеть иметь фикстуры, которые автоматически используются в данном контексте. В конце концов, традиционный стиль unittest-setup предписывает использование такого неявного написания фикстур, и есть шанс, что вы привыкли к нему или он вам нравится.

Вы можете отметить функции фикстуры с помощью @pytest.fixture(autouse=True) и определить функцию фикстуры в том контексте, в котором вы хотите ее использовать. Рассмотрим фикстуру initdir, которая заставляет все тестовые методы класса TestCase выполняться во временном каталоге с предварительно инициализированным samplefile.ini. Наше приспособление initdir само использует встроенное приспособление pytest tmp_path для делегирования создания временного каталога для каждого теста:

# content of test_unittest_cleandir.py
import unittest

import pytest


class MyTest(unittest.TestCase):
    @pytest.fixture(autouse=True)
    def initdir(self, tmp_path, monkeypatch):
        monkeypatch.chdir(tmp_path)  # change to pytest-provided temporary directory
        tmp_path.joinpath("samplefile.ini").write_text("# testdata")

    def test_method(self):
        with open("samplefile.ini") as f:
            s = f.read()
        assert "testdata" in s

Благодаря флагу autouse функция фиксации initdir будет использоваться для всех методов класса, в котором она определена. Это сокращение для использования маркера @pytest.mark.usefixtures("initdir") на классе, как в предыдущем примере.

Запуск этого тестового модуля …:

$ pytest -q test_unittest_cleandir.py
.                                                                    [100%]
1 passed in 0.12s

… дает нам один пройденный тест, потому что функция фиксации initdir была выполнена раньше, чем test_method.

Примечание

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

Приведенные выше примеры usefixtures и autouse должны помочь в подмешивании фикстур pytest в наборы unittest.

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

Примечание

Из-за архитектурных различий между двумя фреймворками, установка и разрушение тестов на основе unittest выполняется на этапе call, а не на стандартных этапах pytest setup и teardown. Это может быть важно понимать в некоторых ситуациях, особенно при рассуждениях об ошибках. Например, если набор, основанный на unittest, обнаружит ошибки во время настройки, pytest не сообщит об ошибках на этапе setup, а вместо этого выдаст ошибку на этапе call.

Вернуться на верх