Всем привет! Я Никита Вандышев, ведущий QA-инженер в Тинькофф Мессенджере.

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

Встроенные фикстуры — хороший инструмент, чтобы не создавать свои велосипеды и эффективно использовать мощь фреймворка, которую хотели передать авторы. Фикстуры помогают в разных случаях: при работе с выводом ошибок, логировании, создании отчетов и так далее. В статье разберем основные встроенные фикстуры и их применение в Pytest.

Потоки ввода и вывода ошибок

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

Capfd — фикстура для работы с потоками стандартного ввода и вывода ошибок на уровне операционной системы. Она позволяет перехватывать не только то, что происходит в python-коде, но и то, что происходит в операционной системе.

Захваченный вывод доступен через вызов метода capfd.readouterr(), который возвращает named tuple с stderr и stdout в виде строк.

def test_system_echo(capfd):
      os.system('echo "hello"')
      captured = capfd.readouterr()
      assert captured.out == "hello\n"

Capfdbinary — тоже работает со стандартным выводом и ошибками, но возвращает именованный кортеж, где сообщения из stderr и stdout представлены в виде байтовых строк.

def test_system_echo(capfdbinary):
      os.system('echo "hello"')
      captured = capfdbinary.readouterr()
      assert captured.out == b"hello\n"

Capsys — фикстура для работы с потоками для стандартного вывода и вывода ошибок на уровне python-кода. Захватывает sys.stdout и sys.stderr из кода и возвращает в виде строк.

 def test_output(capsys):
        print("hello")
        captured = capsys.readouterr()
        assert captured.out == "hello\n"

Capsysbinary — подходит для работы с потоками стандартного вывода на уровне python-кода. Захватывает sys.stdout и sys.stderr из кода и возвращает в виде байтовых строк.

 def test_output(capsysbinary):
        print("hello")
        captured = capsysbinary.readouterr()
        assert captured.out == b"hello\n"

Логирование

Логи — отличный инструмент для дебага, когда дело касается стабильной работы приложения. По ним можно понять, когда и из-за чего в работе системы возник сбой. Pytest имеет несколько фикстур для управления логами из тестов.

Caplog — позволяет работать с логами в python-коде. Дает возможность изменять уровень логирования, перехватывать сообщения, модифицировать их и многое другое.

# Задать уровень логирования
def test_foo(caplog):
    caplog.set_level(logging.INFO)
    for message in caplog.messages:
        assert "for debug level" not in message
# Пример проверки уровня логирования и текста в сообщении лога
def test_baz(caplog):
    func_under_test()
    for record in caplog.records:
        assert record.levelname != "CRITICAL"
    assert "wally" not in caplog.text

Recwarn — возвращает экземпляр класса WarningsRecorder, где будут храниться все warnings, которые были вызваны с помощью warnings.warn в тестовых функциях.

import warnings

def test_check_warnings(recwarn):
    warnings.warn("hello", UserWarning)
    assert len(recwarn) == 1
    warn = recwarn.pop(UserWarning)
    assert issubclass(warn.category, UserWarning)
    assert str(warn.message) == "hello"
    assert warn.filename

Отчеты и документация

Pytest из коробки позволяет генерировать отчеты в формате JUnit XML, поэтому во фреймворке имеется ряд специальных фикстур для управления формированием отчета.

Doctest_namespace — фикстура для работы со встроенной библиотекой doctest. Библиотека помогает сравнить поведение функции с тем, что описано в docstring как ее ожидаемое поведение.

# Объявляем фикстуру в conftest.py
import numpy

@pytest.fixture(autouse=True)
def add_np(doctest_namespace):
    doctest_namespace["np"] = numpy # Добавляет в namespace np ссылку на объект numpy


# Вызываем namespace в numpy.py
def arange():
    """
    >>> a = np.arange(10)
    >>> len(a)
    10
    """
    pass

Record_property — может в junit-отчетe добавить тег property внутрь тега testcase.

def test_function(record_property):
        record_property("example_key", 1)
        assert True

 При формировании junit-отчета мы увидим, что появились новые теги property:

<testcase classname="test_function" file="test_function.py" line="0" name="test_function" time="0.0009">
        <properties>
            <property name="example_key" value="1" />
        </properties>
    </testcase>

Record_testsuite_property — помогает в junit-отчетe добавить тег property внутрь тега test suite.

import pytest

@pytest.fixture(scope="session", autouse=True)
def log_global_env_facts(record_testsuite_property):
    record_testsuite_property("ARCH", "PPC")
    record_testsuite_property("STORAGE_TYPE", "CEPH")


class TestMe:
    def test_foo(self):
        assert True

При формировании junit-отчета мы увидим, что появились новые теги property внутри тега test suite:

 <testsuite errors="0" failures="0" name="pytest" skipped="0" tests="1" time="0.006">
        <properties>
            <property name="ARCH" value="PPC"/>
            <property name="STORAGE_TYPE" value="CEPH"/>
        </properties>
        <testcase classname="test_me.TestMe" file="test_me.py" line="16" name="test_foo" time="0.000243663787842"/>
    </testsuite>

Конфигурация и кеширование

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

Pytestconfig — фикстура уровня session, которая возвращает конфигурацию запуска автотестов. Возвращает инстанс класса pytest.Config.

Можно получить список опций, которые были переданы как флаги при запуске автотестов, получить настройки из конфигурационного файла .ini, доступ к PytestPluginManager(pluginmanager) или к plugin hooks.

# Пример получения значения переданного флага --verbose при запуске pytest
def test_foo(pytestconfig):
    if pytestconfig.getoption("verbose") > 0:
        ...

# Пример получения плагина в фикстуре
pytest.fixture(scope='session')
def testsuite_logger(pytestconfig):
    plugin: Plugin = pytestconfig.pluginmanager.getplugin('testsuite_logger')
    return plugin.testsuite_logger

Сonfig.cache — возвращает экземпляр класса pytest.Cache, позволяет плагинам и фикстурам хранить и извлекать данные во время выполнения тестов. Доступ к config.cache можно получить несколькими способами:

  1. Обратиться через фикстуру config напрямую.

def test_foo(config):
    config.set("name", "Nikita")
    user = config.get("name", None)
    assert user == "Nikita"
  1. Обратиться через фикстуру request с помощью вызова объекта cache

@pytest.fixture
def mydata(request):
    val = request.config.cache.get("value", None)
    if val is None:
        val = 42
        request.config.cache.set(value", val)
    return val
  1. Обратиться через фикстуру pytestconfig с помощью вызова объекта cache. Разработчики рекомендуют использовать именно его, если необходимо получить доступ к кешу в фикстуре.

@pytest.fixture
def my_custom_cache(pytestconfig):
    pytestconfig.cache.set("redis/url", redis_url)

Pytester — фикстура возвращает объект класса Pytester. Дает возможность тестировать плагины, конфигурационные файлы в изолированной среде.

Пример теста с созданием файла .py в рантайме и его последующим запуском

def test_pass_fail(pytester):

    pytester.makepyfile("""
        def test_pass():
            assert 1 == 1

        def test_fail():
            assert 1 == 2
    """)
    result = pytester.runpytest()
    result.stdout.fnmatch_lines([
        '*.F',
    ])
    assert result.ret == 1

Пример теста с созданием ini-файла

 def test_tox_ini_wrong_version(self, pytester: Pytester) -> None:
        pytester.makefile(
            ".ini",
            tox="""
            [pytest]
            minversion=999.0
        """,
        )
        result = pytester.runpytest()
        assert result.ret != 0

Информация о тестах и моки 

В этом разделе — одна из самых интересных фикстур, request, популярная у любителей Pytest.      

Request — предоставляет информацию о тестовой функции.

Можно получить имя тестовой функции, имя класса тестовой функции, модуль, в котором находится тестовая функция, путь до тестовой функции.

Пример с получением информации об имени тестовой функции, каталога, где она расположена, и пути до теста:

def test_node_id(request):
      node = request.node.nodeid
      logging.info(f"\nThis is node:  {node}")

      module_name = request.module.__name__
      logging.info(f"\nThis is module name:  {module_name}")

      function_name = request.function.__name__
      logging.info(f"\nThis is function name:  {function_name}")

      fspath = request.fspath
      logging.info(f"\nThis is fspath :  {fspath}")

Пример с созданием флага и получением значения из передаваемого флага при запуске Pytest:

import pytest

def pytest_addoption(parser):
    parser.addoption(
        "--cmdopt", action="store", default="type1", help="my option: type1 or type2"
    )

@pytest.fixture
def cmdopt(request):
    return request.config.getoption("--cmdopt")

Пример создания декоратора с передачей значения внутрь декоратора:

@pytest.fixture
def some_value(request):
    marker = request.node.get_closest_marker("new_value")
    if marker is None:
        data = None
    else:
        data = marker.args[0]
    return data

@pytest.mark.new_value(42)
def test_fixt(some_value):
    assert some_value == 42

Пример создания классовой фикстуры с помощью request:

import pytest
from selenium import webdriver


@pytest.fixture(scope="class")
def chrome_driver_init(request):
    chrome_drv = webdriver.Chrome()
    request.cls.driver = chrome_drv
    yield
    chrome_drv.close()
    
@pytest.mark.usefixtures("chrome_driver_init")
class TestClass:

    def test_open_url(self):
        self.driver.get("ya.ru")
        print(self.driver.title)

Monkeypatch — дает возможность модифицировать объекты, списки и переменные среды выполнения. С ее помощью можно добавить атрибуты, удалить атрибуты, изменить env-переменные, поменять системные пути.

import os

monkeypatch.setattr(os, "getcwd",lambda: "/")

Работа с временными каталогами

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

Tmp_path — возвращает экземпляр класса pathlib.Path, который будет выступать как путь к временному каталогу. Путь будет уникальным для каждого вызова тестовой функции, потому что создаются вложенные каталоги под каждый тест.

 def test_create_file(tmp_path):
            words = "hello world!"
            d = tmp_path / "sub"
            d.mkdir()
            p = d / "hello.txt"
            p.write_text("hello world!")
            assert p.read_text() == words

Tmp_path_factory — фикстура типа session, которая используется для генерации временного каталога из любого теста или фикстуры. Удобно использовать для создания временных объектов, которые будут использоваться в тестовых функциях. 

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

import pytest
from PIL import Image

@pytest.fixture(scope="session")
def create_avatar(tmp_path_factory):
    avatar = Image.new('RGB', (60, 30), color = 'red')
    fn = tmp_path_factory.mktemp("data") / "avatar_img.png"
    avatar.save(fn)
    return fn

def test_avatar(create_avatar):
    loaded_avatar = load_avatar_to_user_icon(create_avatar)

Устаревшие фикстуры

Pytest имеет ряд устаревших фикстур. Они используют библиотеку py, в частности py.path, которая сейчас находится в режиме поддержки и больше не развивается.

Testdir — делает то же самое, что и pytester, но возвращает экземпляры с устаревшими py.path.local-объектами. Разработчики Pytest рекомендуют использовать pytester вместо testdir.

Tmpdir — возвращает py.path.local. Рекомендуется использовать вместо нее более свежую фикстуру tmp_path.

Tmpdir_factory — возвращает обертку над py.path.local. Рекомендуется использовать вместо нее более свежую фикстуру tmp_path_factory.

Заключение

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

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

А если остались вопросы или хотите поделиться своим опытом использования — пишите в комментариях!

Комментарии (1)


  1. Andrey_Solomatin
    05.07.2023 10:12

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

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

    Не удаляются. Точнее удаляются, но не сразу. Там интерестная логика, стоит её описать в статье, так как это бывает полезно знать.

    Monkeypatch

    Пример использования не содержит теста. Его можно переписть намного проще.

    os.getcwd = lambda: '/'

    Вся сила этой фикстуры, в том что она возвращает старое значение после завершения теста.