14. Pytest. Часть II - qa-guru/knowledge-base GitHub Wiki

Данный раздел разделен на несколько частей(каждый пункт - это ссылка на соответствующий раздел):


Pytest Hooks (Хуки)

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

Основные хуки в Pytest:

  • pytest_addoption — добавляет новые опции при запуске тестов;
  • pytest_configure — изменяет что-нибудь в конфигурации;
  • pytest_sessionstart — действия перед стартом всех тестов;
  • pytest_generate_tests — изменяем параметризацию тестов;
  • pytest_collection_modifyitems — редактирование собранных тестов;
  • pytest_runtestloop — хуки во время выполнения тестов;
  • pytest_sessionfinish — все тесты завершились.

Все хуки в Pytest прописываются в файле conftest.py.

Примеры использования хуков:

pytest_addoption

Нажать, чтобы раскрыть
# conftest.py

def pytest_addoption(parser, pluginmanager):
    parser.addoption(
        "--browser",
        help="This is browser",
        required=False,
        default="chrome",
        choices=['chrome', 'firefox', 'all'],
    )
    parser.addoption(
        "--mobile-only",
        type=bool,
        required=False,
        default=False,
    )

Аргумент parser — это объект, который позволяет добавлять новые опции. В данном случае добавляются две опции: --browser и --mobile-only. Первая опция принимает значение из списка ['chrome', 'firefox', 'all'], вторая опция принимает булево значение.

pytest_configure

Нажать, чтобы раскрыть
# conftest.py
def pytest_configure(config):
    if config.getoption("--browser") == "chrome":
        config.option.browser = "chrome-98"

В данном примере, если при запуске тестов передать опцию --browser=chrome, то внутри тестов будет доступна переменная browser со значением chrome-98.

pytest_sessionstart

Нажать, чтобы раскрыть
# conftest.py
def pytest_sessionstart(session):
    print("Session started!")

В данном примере при старте всех тестов будет выведено сообщение Session started. Этот хук будет вызван один раз перед началом выполнения всех тестов. К примеру ее можно использовать для подготовки тестовых данных перед началом выполнения тестов.

pytest_sessionfinish

Нажать, чтобы раскрыть
# conftest.py
def pytest_sessionfinish(session, exitstatus):
    print("Session finished!")

В данном примере при завершении всех тестов будет выведено сообщение Session finished. Этот хук будет вызван один раз после завершения всех тестов. К примеру его можно использовать для очистки данных после выполнения всех тестов.

pytest_collection_modifyitems

Нажать, чтобы раскрыть

Этот хук запускается после сбора всех тестов и позволяет изменить порядок выполнения тестов, добавить маркеры, пропустить тесты и т.д.

# conftest.py
def pytest_collection_modifyitems(config, items: list[pytest.Item]):
    """
    Skip other tests if mobile-only option is True
    Sort tests if required
    """
    items.sort(key=lambda x: x.name, reverse=True)

    for item in items:
        if "mobile" not in item.name and config.getoption("--mobile-only"):
            item.add_marker(pytest.mark.skip("Мы запустили только мобильные тесты"))

    items.sort(key=lambda x: "desktop" in x.own_markers)

В данном примере тесты будут отсортированы по имени в обратном порядке. Если передана опция --mobile-only, то все тесты, в названии которых нет слова mobile, будут пропущены. Также тесты будут отсортированы по наличию маркера desktop.

pytest_runtestloop

Нажать, чтобы раскрыть
# conftest.py
@pytest.hookimpl(trylast=True, hookwrapper=True)
def pytest_runtest_call(item):
    """Allure dynamic title"""
    yield
    allure.dynamic.title(" ".join(item.name.split("_")[1:]).title())

Где trylast=True говорит о том, что хук будет вызван последним, а hookwrapper=True позволяет использовать yield внутри хука.

В данном примере при запуске каждого теста будет меняться его название. Название теста будет браться из его имени, разделенного символом _, и преобразовываться в заголовок. А именно из названия тестов test_login_page и test_main_page будет получено Login Page и Main Page.

pytest_generate_tests

Нажать, чтобы раскрыть
# conftest.py
def pytest_generate_tests(metafunc: pytest.Metafunc):
    if 'browser' in metafunc.fixturenames:
        if metafunc.config.getoption("--browser") == "all":
            metafunc.parametrize("browser", ["chrome-98", "firefox"], indirect=True)
        else:
            metafunc.parametrize("browser", [metafunc.config.getoption("--browser")], indirect=True)

В данном примере при запуске тестов будет изменена параметризация тестов. Если передана опция --browser=all, то тесты будут запущены на двух браузерах: chrome-98 и firefox. В противном случае будет запущен только один браузер, который передан в опции --browser.

pytest_runtest_makereport

Нажать, чтобы раскрыть
# conftest.py
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """Make screenshot"""
    outcome = yield
    result = outcome.get_result()
    if item.when == "call" and result.failed is True:
        make_screenshot()

В данном примере при падении теста будет вызвана функция make_screenshot(). Также можно добавить другие действия при падении теста.

Фикстура pytest_report_teststatus

Нажать, чтобы раскрыть

Эта фикстура позволяет изменить статус теста. Например, можно изменить статус теста на PASSED, FAILED, SKIPPED.

# conftest.py
def pytest_report_teststatus(report, config):
    if report.when == "call":
        if report.passed:
            report.outcome = "PASSED1"
        elif report.failed:
            report.outcome = "FAILED2"
        elif report.skipped:
            report.outcome = "SKIPPED3"

В данном примере при запуске тестов будет изменен статус теста. Если тест пройден успешно, то его статус будет изменен на PASSED1, если тест упал, то его статус будет изменен на FAILED2, если тест пропущен, то его статус будет изменен на SKIPPED3.

Примерное дерево хуков

Дерево последовательности выполнения тестов и вызова хуков (ссылка):

root
└── pytest_cmdline_main
    ├── pytest_plugin_registered
    ├── pytest_configure
    │   └── pytest_plugin_registered
    ├── pytest_sessionstart
    │   ├── pytest_plugin_registered
    │   └── pytest_report_header
    ├── pytest_collection
    │   ├── pytest_collectstart
    │   ├── pytest_make_collect_report
    │   │   ├── pytest_collect_file
    │   │   │   └── pytest_pycollect_makemodule
    │   │   └── pytest_pycollect_makeitem
    │   │       └── pytest_generate_tests
    │   │           └── pytest_make_parametrize_id
    │   ├── pytest_collectreport
    │   ├── pytest_itemcollected
    │   ├── pytest_collection_modifyitems
    │   └── pytest_collection_finish
    │       └── pytest_report_collectionfinish
    ├── pytest_runtestloop
    │   └── pytest_runtest_protocol
    │       ├── pytest_runtest_logstart
    │       ├── pytest_runtest_setup
    │       │   └── pytest_fixture_setup
    │       ├── pytest_runtest_makereport
    │       ├── pytest_runtest_logreport
    │       │   └── pytest_report_teststatus
    │       ├── pytest_runtest_call
    │       │   └── pytest_pyfunc_call
    │       ├── pytest_runtest_teardown
    │       │   └── pytest_fixture_post_finalizer
    │       └── pytest_runtest_logfinish
    ├── pytest_sessionfinish
    │   └── pytest_terminal_summary
    └── pytest_unconfigure

Параллельное выполнение тестов(xdist)

pytest-xdist - это библиотека, которая позволяет запускать тесты параллельно.

Для успешного параллельных тестов важно помнить:

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

У xdist есть аргументы запуска. Основным таким аргументом является -n numprocesses, который принимает количество параллелей для запуска. Также можно передать auto для автоматической настройки. Остальные аргументы ситуативные и редко используются, получить информация о них можно с помощью команды help.

Пример: К примеру, у нас есть тест, который выполняется 6 раз и каждое выполнение занимает одну секунду:

@pytest.mark.parametrize("n", range(6))
def test_sleep(n):
    time.sleep(1)

Если запустить тест обычным способом с помощью команды pytest -v -k test_sleep, то тесты начнут выполняться поочерёдно и это займет приблизительно 8 секунд. Если этот же тест запустить с помощью команды pytest -v -k test_many_params -n 2, то тесты будут запускаться в два потока и на выполнение уже понадобится в два раза меньше времени.

Важно учесть: Количество потоков зависит от количества ядер процессора. Если у вас 4 ядра, то можно запустить 4 потока. Если у вас 8 ядер, то можно запустить 8 потоков. Но не стоит забывать, что чем больше потоков, тем больше нагрузка на процессор и память.

Если вы используете фикстуру с scope="session" то при параллельном запуске тестов, фикстура будет исопльзоваться во всех потоках. Поэтому важно учитывать это при написании фикстур.

Пример использования фикстуры с scope="session" без учета параллельного запуска тестов:

Пример использования фикстуры с scope="session" с учетом параллельного запуска тестов:

⚠️ **GitHub.com Fallback** ⚠️