Pytest-фикстуры на человеческом
Как понять фикстуры и как начать их использовать в своих проектах. Обьясняю эту тему начинающим, ибо именно в ней часто случаются сложности.
Привет сообщество,
Я хотел бы поделиться с Вами своим обьяснением того, как понять фикстуры и как начать их использовать в своих проектах, тем самым начать радоваться жизни)
Вероятно, даже продвинутый QA Automation найдет что-то новое, но моя цель обьяснить на пальцах эту тему начинающим, ибо именно в ней зачастую происходят затыки.
Файл conftest.py
Это специальный pytest файл, в который он заглядывает еще перед тем как запустить тесты. Поэтому в основном он используется для создания фикстур внутри нашего проекта.
Данный файл обычно находиться в корне проекта, возьмем к примеру базовую структуру PageObject:
Что такое фикстура
Фикстура – это ресурс или объект, который можно рассматривать как набор условий или предопределенное состояние, необходимое тесту для правильного выполнения, зачастую фикстуры создаются, чтобы генерировать какие-то данные еще до теста и возвращать их для использования в тесте или перед тестом, например:
- Создавать подключение к базе данных перед тестом и отключаться от нее после завершения теста
- Инициализировать драйвер-браузера и закрывать сессию после завершения теста
- Авторизовываться перед запуском теста и не тратить время на логин
- Создавать новый аккаунт перед тестом, использовать его в тесте и по завершению теста удалять его т.д
Так же важно знать, что фикстуры могут быть общими для нескольких тестов.
Использование фикстур через return и ее передача в тест в качестве аргумента
Начнем с простого примера:
Как Вы можете видеть, у нас есть максимально простая функция, она возвращает образно, подключение к базе данных.
Что, если мы хотим использовать ее, как обертку для теста, т.е подключаться к базе данных еще перед тестом, а уже в тесте использовать эти данные?
В голову приходит идея импортировать эту функцию и вызывать ее в каждом тесте или сложнее, сделать из этой функции декоратор, обертку для тестов (как раз то, что нам нужно).
Тк вот в этом нам поможет фикстурирование, т.е превращение нашей функции в фикстуру (по сути так называется декоратор, но в контексте pytest)
Для того, чтобы функцию зарегистрировать как фикстуру, в pytest есть специальный маркер (декоратор) @pytest.fixture, его нужно прописать над нужной функцией.
Напомню все общие фикстуры мы пишем в файле conftest.py, они будут видны всем тестовым классам по умолчанию.
Но как это использовать? Есть несколько путей, мы начнем с самого простого, а именно будем прокидывать фикстуру в качестве аргумента внутрь нашего теста / тестового метода.
В итоге перед запуском теста, в консоли мы увидим то самое сообщение «Соединение с базой данных установлено», что будет означать выполнение подключения к БД или любого другого кода перед тестом.
Как вы могли заметить, никаких импортов нет, тестовые файлы автоматически находят фикстуру из conftest.py без каких-либо импортов, достаточно пробросить фикстуру в качестве аргумента в тестовый метод.
Но что, если фикстура возвращает несколько объектов, например, сгенерированный логин и пароль, то как их использовать в тесте?
Исходя из того, как работает python, мы знаем, что он в данном случае вернет кортеж данных:
Соответственно, чтобы передать данные в тест, нужно так же передать фикстуру в качестве аргумента и обращаться к элементам кортежа по индексу:
Все это классно, но не совсем удобно, потому что, чтобы узнать к чему мы обращаемся, нужно идти в саму фикстуру, так как из строчки generate_dataничего не будет понятно.
Выход следующий, можно возвращать значения в виде словаря и обращаться к элементам по ключу:
Вот теперь стало намного понятнее)
Использование фикстур через request.cls и ее вызов с помощью маркера (декоратора)
Следующий способ использования фикстур — это request.cls
Постараюсь объяснить максимально простым языком, для этого возьмем тот же пример с генерацией логина и пароля:
- В качестве аргумента внутрь нашей фикстуры прокидываем request
- Назначим переменную с помощью request.cls.имя_переменной
- Суть в том, что, когда вы объявляете переменную через request.cls.имя_переменной, в тестовом классе автоматически будет создаваться такой атрибут. Это то же самое, что если бы вы напрямую объявили их в init тестового класса:
test_example.pyrequest — создатьcls — внутри классаимя_переменной — атрибут классасоздать.внутри_класса.имя_переменной = request.cls.loginВ случае использования request.cls, оператор return прописывать не нужно, но всегда нужно передавать в фикстуру request.Теперь переходим к этапу использования. В случае, если вы используете вышеописанный метод, то не нужно прокидывать фикстуру в тест в качестве аргумента, тут все интереснее, мы будем использовать специальный pytest-маркер (декоратор):@pytest.mark.usefixtures(«имя фикстуры») — маркер (декоратор) для вызова фикстурыМаркер (декоратор) прописывается либо над нужным тестом, в случае, если вы хотите генерировать данные из фикстуры только для одного теста, или над классом, если хотите генерировать новые данные для каждого теста.import pytestimport time@pytest.fixturedef generate_data(request): request.cls.login = f"autotest_{time.time()}@hyper.org" # Генерирует логин request.cls.password = "512" # Назначает парольconftest.pyimport pytest@pytest.mark.usefixtures("generate_data") # Вызываем фикстуру над классомclass TestExample:ttdef test_1(self):tt print(self.login) # Сюда передастся логинtt print(self.password) # А сюда парольtest_example.pyЕсли вы вызвали фикстуру через маркер (декоратор) и использовали request.cls для создания данных в фикстуре, то логин и пароль будут доступны через self параметр напрямую!Надеюсь разобрались) Теперь по аналогии сделаем фикстуру для инициализации драйвера:import pytestimport timefrom selenium import webdriverfrom webdriver_manager.chrome import ChromeDriverManagerfrom selenium.webdriver.chrome.service import Service@pytest.fixturedef get_driver(request): service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service) request.cls.driver = driverconftest.pyТеперь, вызвав с помощью маркера (декоратора) фикстуру непосредственно в тесте, вы можете спокойно обращаться к драйверу и его методам через self.driver.@pytest.mark.usefixtures**("get_driver") # Вызываем фикстуру над тестомclass TestExample:ttdef test_1(self):tt self.driver.get("https://yandex.ru") # Работаем с полученным обьектом драйвераtest_example.pyАвтоматическое использование фикстурТут все просто, у фикстуры есть параметр:autouse — данный параметр принимает в качестве значения True или FalseВ случае, если выставить autouse=True, фикстура будет вызываться абсолютно для каждого теста в проекте автоматически, без явного ее вызова где-либо.import pytestimport timefrom selenium import webdriverfrom webdriver_manager.chrome import ChromeDriverManagerfrom selenium.webdriver.chrome.service import Service@pytest.fixture(autouse=True) # Пока не обращаем внимания на эту строкуdef get_driver(request): service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service) request.cls.driver = driverconftest.pyimport pytestclass TestExample:ttdef test_1(self):tt self.driver.get("https://vk.com") # И никаких явных вызовов фикстурыtest_example.pyСоответсвенно, чтобы избежать проблем и конфликтов между разными фикстурами, я рекомендую использовать этот атрибут исключительно для инициализации драйвера.Пред- и постусловия в фикстурахДля того, чтобы фикстура делала что-то перед тестом, например, инициализация драйвера, и после теста, например, закрытие браузера, существует специальная функция:yield — это разделитель, все, что написано над ним, будет исполнено до теста, все, что ниже — после теста.import pytestimport timefrom selenium import webdriverfrom webdriver_manager.chrome import ChromeDriverManagerfrom selenium.webdriver.chrome.service import Service@pytest.fixture(autouse=True)def get_driver(request): service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service) request.cls.driver = driverttyield driver.quit()conftest.pyЕсли мы вспомним как создается стандартный декоратор, то он принимает в себя функцию. И все что над ней — это предусловия, а то, что после — это постусловия.def decorator(test): # Принимает в себя функциюttdef wrapper(): print("Перед тестом") # Предусловия test() # Функция которую декарируем print("После теста") # Постусловияttreturn wrapper@decorator # Применяем декораторdef hello(): print("Привет")example.pyСоответственно, все аналогично, во время запуска тестов, в фикстуру вместо yield подставляется наш тест.Область видимости фикстурОбласть видимости определяет, как применяется фикстура, для всего набора тестов (для тестового класса) единожды или для каждого теста (тестового метода) отдельно.За область видимости отвечает параметр фикстуры:scope — определяет область применения фикстурыВ целом, для 99% случаев, достаточно знать лишь 2 области видимости:1. scope=»class» — фикстура будет вызвана один раз для всех тестов внутри тестового класса.— Пример 1: Возьмем фикстуру инициализации драйвераimport pytestimport timefrom selenium import webdriverfrom webdriver_manager.chrome import ChromeDriverManagerfrom selenium.webdriver.chrome.service import Service@pytest.fixture(autouse=True, scope="class")def get_driver(request): service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service) request.cls.driver = driverttyield driver.quit()conftest.pyИ для примера сделаем 2 тестаimport pytestclass TestExample:ttdef test_1(self): self.driver.get("https://vk.com")ttttdef test_2(self): self.driver.get("https://ya.ru")test_example.pyТогда браузер откроется, и в этом же окне будут проходить все тесты. Браузер закроется только тогда, когда пройдут все тесты, а это уже, как минимум, не позволит нам запустить тесты параллельно, ибо они будут друг друга перебивать.— Пример 2: Генерация логинаimport pytestimport time@pytest.fixture(scope="class")def generate_login(request): request.cls.login = f"autotest_{time.time()}@hyper.org" # Генерирует логинconftest.pyПри использовании фикстуры, логин сгенирируется один раз и будет одинаковым для всех тестов внутри класса.import pytest@pytest.mark.usefixtures("generate_login") # Вызов фикстуры генерации логинаclass TestExample:ttttdef test_1(self): print(self.login)ttttdef test_2(self): print(self.login)test_example.pyВ результате, мы получим один и тот же логин в каждом тесте. Это может помочь в зависимых тестах, но тем не менее, атомарность — залог качественных тестов. Тут мы и подходим ко второй области видимости.2. scope=»function» — фикстура будет вызываться для каждого теста по отдельности, атомарно.Это означает, что для каждого теста, в случае фикстуры с драйвером например, будет открываться новая сессия браузера, а при генерации данных, для каждого теста будут генерироваться новые данные.— Пример 1: Возьмем фикстуру инициализации драйвераimport pytestimport timefrom selenium import webdriverfrom webdriver_manager.chrome import ChromeDriverManagerfrom selenium.webdriver.chrome.service import Service@pytest.fixture(autouse=True, scope="function")def get_driver(request): service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service) request.cls.driver = driverttyield driver.quit()conftest.pyИ для примера сделаем 2 тестаimport pytestclass TestExample:ttdef test_1(self): self.driver.get("https://vk.com")ttttdef test_2(self): self.driver.get("https://ya.ru")test_example.pyТогда браузер будет инициализироваться для каждого теста отдельно, что позволит запускать тесты параллельно, ну или просто не сталкивать их друг с другом и в прямом смысле запускать их атомарно.— Пример 2: Генерация логинаimport pytestimport time@pytest.fixture(scope="function")def generate_login(request): request.cls.login = f"autotest_{time.time()}@hyper.org" # Генерирует логинconftest.pyПри использовании фикстуры, будет генирироваться новый логин для каждого тестаimport pytest@pytest.mark.usefixtures("generate_login") # Вызов фикстуры генерации логинаclass TestExample:ttdef test_1(self): print(self.login)ttttdef test_2(self): print(self.login)test_example.pyВ результате, полностью разные данные в каждом новом тесте.ЗаключениеЯ надеюсь, что данная статься наконец помогла Вам разобраться с тем, как устроены фикстуры и как с ними работать.В статье я использовал максимально человеческий язык, избегая заумных технических терминов, ибо куда важнее объяснить так, чтобы человек разобрался, а не так чтобы он прочитав пол статьи бросил ее и пошел искать другую.