Глобальная фикструра с передачей данных в pytest

Всем привет, недавно познакомился с py.test как альтернатива для unittest. Очень понравился, плюс красивые отчетики в allure.

Вопрос собственно таков:
Как организовать фикстуру, которая глобально, каждому модулю/классу будет передавать активный инстанс webdriver через conftest.py и к нему нормально можно будет обратится

На офф документации нашел только обход метод с добавлением к классам атрибута, меня такой вариант не устраивает. Практически 20 классов в каждом по 10-15 тест методов, изменять устоявшиеся сигнатуры как то не охота. Плюс ко всему, проект разростается с огромной скоростью, и на данный момент все функционирует, только нету централизованного запуска.

py.test + allure бомба, согласен +1 :smile:

А как у вас сейчас передается инстанс вебдрайвера?

А в чем проблема сделать реализацию управления инициализацией вебдрайвера (можно например по принципу singleton) через один какой-то класс в отдельном модуле и потом его импортировать в нужное место, где необходимо использовать инстанс вебдрайвера?

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

Сейчас у меня фикстура лежит на уровне модуля (пережитки unittest и green).

В самой фикстуре, была обработка cmd (какой ключ, такой и браузер юзаем) и глобально (на уровне модуля) передача самого драйвера в каждый класс.

Что хочется:

Универсальный conftest.py который будет отдавать объект драйвера, в независимости от вложенности пакетом/модулей/классов самих тестов.

def setup_module():

    """Подготовка модуля и обработка CLI"""

    global driver
    driver = webdriver.Ie()
    if '*ie' in sys.argv:
        driver = webdriver.Ie()
    elif '*chrome' in sys.argv:
        driver = webdriver.Chrome()
    elif '*firefox' in sys.argv:
        driver = webdriver.Firefox()

def teardown_module():
    driver.close()

сами тесты

class Test_Home_Page(Check):

    """ Testing Home page"""

    @classmethod
    def setup_class(cls):
        cls.driver = driver
        cls.link = "http://localhost:8888/"
        cls.driver.get(cls.link)

    @allure.story('Ответа сервера')
    def test_status_code(self):
        assert(self.status_code())

    @allure.story('Наличие head-тега страницы')
    def test_header(self):
        assert(self.header())

Пока, все эти классы в одном модуле, но нужно их разнести, плюс - будет множество других модулей/пакетов в скором времени.

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

самое главное забыл!)

#conftest.py

import pytest
from selenium import webdriver

def pytest_addoption(parser):
    parser.addoption("--browser", action="store", default="ie", help="Type of browser: ie, chrome, firefox")


@pytest.fixture(scope="session", autouse=True)
def driver_up_session(request):
    driver = None
    browser = request.config.getoption("--browser")
    if browser == 'ie':
        driver = webdriver.Ie()
    elif browser == 'chrome':
        driver = webdriver.Chrome()
    elif browser == 'firefox':
        driver = webdriver.Firefox()
    def browser_down():
        driver.close()
    request.addfinalizer(browser_down)
    return driver

Фикстуру можно передать в тест напрямую:

def test_some_thing(fixture_name):
 тут уже идет тест

Так же можно использовать декоратор для класса:

@pytest.mark.usefixtures("cleandir")
class TestSomeThings()

в таком случае, все тестовые методы будут использовать эту фикстуру.

Так же у фикстур есть параметр autouse, но с ним надо быть сильно аккуратнее.

Подробнее вот тут:
https://pytest.org/latest/fixture.html

У меня есть session-фикстура, (см. код conftest), мне нужно в каждый класс передать, возвращаемый фикстурой результат.

иными словами что я хочу.

В начале запуска тестов, получить ссылку на екземпляр класса webdriver, провести все тесты, и закрыть его finalize секцией фикстуры.

Для чего вы группируете тесты по классам? Есть какое-либо еще действие, которое надо совершить перед тестами кроме инициализации драйвера?

Логически разделять тесты лучше по разным файлам.

Если нет - просто передайте имя фикстуры как параметр в тест.

то есть
test_some_thing(driver_up_session):

а дальше можно использовать driver_up_session в теле теста как переданный параметр.

Так как scope для фикструры стоит session - драйвер по факту будет создан один раз и просто будет передаваться между тестами. Финализер будет вызван в конце сессии, а не в конце теста.

Да, каждый класс тестов, это определенная страница или функциональный узел.
То, что фикстуру можно передать как аргумент в функцию я понимаю.

Если я декорирую класс фикстурой, я не могу потом к ней обратится.

Я пробовал фикстуру, передавать с setup_module(), что бы из него сделать драйвер глобальным для модуля:

driver = None
def setup_module(driver_up_session):
    global driver
    driver = driver_up_session


class Test_1:
    @classmethod
    def setup_class(cls):
        cls.driver = driver

    def test_1(self):
        self.driver.get("http://localhost:8888/about.htm")

при этом получаю ошибку вида:

self = <day_first.test_1.Test_1 object at 0x034607F0>

    def test_1(self):
>       self.driver.get("http://localhost:8888/about.htm")
E       AttributeError: 'module' object has no attribute 'get'

Вот теперь я действительно ничего ен понимаю, что происходит в фикстурах, и почему это объект типа web-driver внезапно стал module типа

Я похоже Вас запутал :smile:

Можно сделать микстуру, которая будет задавать какое-либо значение для класса.

Пример лучше описан вот тут:

https://pytest.org/latest/unittest.html

НО! Так можно делать только если scope стоит class. Что означает, что драйвер будет пересоздавать перед каждым классом и убиваться в конце.

Возможно есть еще какие-то методы, но я бы посмотрел в сторону прямой передачи микстуры тесту

В принципе можно сделать драйвер синглтоном и сделать через scope class, не убивая драйвер в конце каждого класса, но это как-то уже странно

Спасибо, такое решение, конечно имеет место быть, но я честно несколько поражен что нет нормального способа передачи.

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

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

С другой стороны, мне стало интересно отчего я получаю непонятные результаты из кода (см. пред пост).

В общем пролазил пол дня, ответ так и не нашел на свою проблему(

Объяснять долго не буду, но вот сделал небольшой примерочный код, я надеюсь это то что Вам требуется. Если что пишите.

confttest.py

import pytest
from selenium import webdriver


class DriverManager(object):

    def __init__(self):
        self._instance = None

    def start(self, type='ff'):
        # implement logic to create instance that depends on condition
        self._instance = webdriver.Firefox()
        return self._instance

    @property
    def instance(self):
        if not self._instance:
            self.start()
        return self._instance

    def stop(self):
        self._instance.close()


@pytest.fixture(scope="module")
def driver():
    return DriverManager()

common.py

import pytest


class BaseTest(object):

    @pytest.fixture(scope="class", autouse=True)
    def manage_driver(self, request, driver):
        driver.start()
        request.addfinalizer(driver.stop)

test_class_1.py

from common import BaseTest


class TestClass1(BaseTest):

    def test_1_1(self, driver):
        driver.instance.get('http://lessons2.ru')

    def test_1_2(self, driver):
        driver.instance.get('http://automated-testing.info')


class TestClass2(BaseTest):

    def test_2_1(self, driver):
        driver.instance.get('http://lessons2.ru/python-for-testers/')

    def test_2_2(self, driver):
        driver.instance.get('http://twitter.com/autotestinfo')

test_class_2.py

from common import BaseTest


class TestClass3(BaseTest):

    def test_3_1(self, driver):
        driver.instance.get('http://www.facebook.com/autotestinfo')

    def test_3_2(self, driver):
        driver.instance.get('http://vk.com/autotestinfo')

Весь код выложил в gist на всякий случай https://gist.github.com/polusok/50398925888827306b0e.

2 лайка

Некропостер намекает тем, у кого возникнет подобная проблема:

# conftest.py
import pytest

@pytest.fixture(scope='class')
def d(request):
    from selenium import webdriver

    driver = webdriver.Firefox()
    request.cls.driver = driver

    def fin():
        driver.quit()

    request.addfinalizer(fin)
# BaseCase.py
from unittest import TestCase
import pytest


@pytest.mark.usefixtures('d')
class BaseCase(TestCase):
    pass
# LoginTest.py

from BaseCase import BaseCase

class TestGoodLogin(BaseCase):
    def test_login_good(self):
        login_page = LoginPage(self.driver)
        profile_page = login_page.login('email@example.com', '123456')

        assert profile_page.user_navbar
        profile_page.logout()

Все классы наследованные от BaseCase - будут иметь инстанс драйвера (self.driver)
Пример рабочий. Юзается в контексте PageObject/PageElement, но без удобного автокомплита со стороны PyCharm =)

Зачем городить тройное оборачивание с синглтоном - мне не понятно…

2 лайка

Спасибо, правда не актуально уже)
Сделал по другому через создание приложения webdriver, на момент вызова тестов.

Ну я тоже тему методом поиска нарыл. Кому-то точно пригодится =)

Какой-то неведомый способ прям)

Почему не ведомый?
Так же как и вы поднимаю сессионную фикстуру, и отдаю ее моему приложению, в приложении сложены все пейдж-обжекты страниц. Удобно и красиво

class TestClass3(BaseTest):
    def setup_class(cls):
        #подскажите как сюда передать driver ?


    def test_3_1(self, driver):
        driver.instance.get('http://www.facebook.com/autotestinfo')

    def test_3_2(self, driver):
        driver.instance.get('http://vk.com/autotestinfo')

Вы используете py unit или py.test?

py.test

Лучше опишу свою хотелку. Перевожу тесты с codeception + php на связку python + py.test.
Тесты разбиты по классам, по функционалу + логическая связь есть между тестами.
Так вот, перед прогоном нужно входить в систему под определенным пользователем, после теста выходить из системы. Думал это сделать через setup_class и teardown_class но как я понял это для unit стиля http://pytest.org/2.2.4/xunit_setup.html
Как это правильней и лучше реализовать?

Контекст и его подчистка во многих современных тестовых фреймворках\модулях доступна на уровне методов\классов\неймспейсов.

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

Логины должны быть разные. Тест доступа функционала для роли под конкретным пользователем.

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