14. Pytest. Часть II - qa-guru/knowledge-base GitHub Wiki
Данный раздел разделен на несколько частей(каждый пункт - это ссылка на соответствующий раздел):
- Pytest Hooks (Хуки)
- Фикстура pytest_report_teststatus
- Примерное дерево хуков
- Параллельное выполнение тестов(xdist)
Хуки в Pytest — это специальные функции, которые выполняются в определенные моменты времени. В отличие от некоторых других фреймворков, в Pytest есть возможность влиять на тест во время его выполнения. Также важно учитывать, что хуки применимы в основном в больших проектах. В простых тестах они бывают лишними.
Основные хуки в Pytest:
-
pytest_addoption
— добавляет новые опции при запуске тестов; -
pytest_configure
— изменяет что-нибудь в конфигурации; -
pytest_sessionstart
— действия перед стартом всех тестов; -
pytest_generate_tests
— изменяем параметризацию тестов; -
pytest_collection_modifyitems
— редактирование собранных тестов; -
pytest_runtestloop
— хуки во время выполнения тестов; -
pytest_sessionfinish
— все тесты завершились.
Все хуки в Pytest прописываются в файле conftest.py
.
Примеры использования хуков:
Нажать, чтобы раскрыть
# 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']
, вторая опция принимает булево значение.
Нажать, чтобы раскрыть
# conftest.py
def pytest_configure(config):
if config.getoption("--browser") == "chrome":
config.option.browser = "chrome-98"
В данном примере, если при запуске тестов передать опцию --browser=chrome
, то внутри тестов будет доступна переменная browser
со значением chrome-98
.
Нажать, чтобы раскрыть
# conftest.py
def pytest_sessionstart(session):
print("Session started!")
В данном примере при старте всех тестов будет выведено сообщение Session started
.
Этот хук будет вызван один раз перед началом выполнения всех тестов. К примеру ее можно использовать для подготовки тестовых данных перед началом выполнения тестов.
Нажать, чтобы раскрыть
# conftest.py
def pytest_sessionfinish(session, exitstatus):
print("Session finished!")
В данном примере при завершении всех тестов будет выведено сообщение Session finished
.
Этот хук будет вызван один раз после завершения всех тестов. К примеру его можно использовать для очистки данных после выполнения всех тестов.
Нажать, чтобы раскрыть
Этот хук запускается после сбора всех тестов и позволяет изменить порядок выполнения тестов, добавить маркеры, пропустить тесты и т.д.
# 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
.
Нажать, чтобы раскрыть
# 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
.
Нажать, чтобы раскрыть
# 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
.
Нажать, чтобы раскрыть
# 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()
. Также можно добавить другие действия при падении теста.
Нажать, чтобы раскрыть
Эта фикстура позволяет изменить статус теста. Например, можно изменить статус теста на 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
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"
с учетом параллельного запуска тестов: