Есть отличная удаленная работа для php+codeception+jenkins+allure+docker спецов. 100% remote! Присоединиться к проекту

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

pytest
python
webdriver
Теги: #<Tag:0x00007f7b62dfb508> #<Tag:0x00007f7b62dfb3c8> #<Tag:0x00007f7b62dfb260>

(Goshko Nazar) #1

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

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

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


Не могу сложить пазлы (webdriver + pytest + webium)
(Mykhailo Poliarush) #2

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

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

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

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


(Goshko Nazar) #3

Сейчас у меня фикстура лежит на уровне модуля (пережитки 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

(Urtow) #4

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

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

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

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

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

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

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


(Goshko Nazar) #5

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

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

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


(Urtow) #6

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

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

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

то есть
test_some_thing(driver_up_session):

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

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


(Goshko Nazar) #7

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

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

Я пробовал фикстуру, передавать с 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 типа


(Urtow) #8

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

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

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

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

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

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

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


(Goshko Nazar) #9

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

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

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

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

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


(Mykhailo Poliarush) #10

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

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.


(ex3me0) #11

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

# 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 =)

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


(Goshko Nazar) #12

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


(ex3me0) #13

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

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


(Goshko Nazar) #14

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


(Vitali) #15
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')


(Goshko Nazar) #16

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


(Vitali) #17

py.test

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


(Евгений Бухгаммер) #18

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

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


(Vitali) #19

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


(ex3me0) #20

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