Какой должна быть идеальная структура проекта автотестов?

Но мы не пишем тесты “на бумаге”, ничего страшного в создании обьекта страницы в автоматизированном тесте я не вижу :slight_smile:

А вот это:

open(url)
.loginWith(user)
.doSmthElse();

Я так понял у вас написано в каждом тесте? Правда .doSmthElse(); для каждого разный, так?

Я и сам об этом думал 100 раз, но у меня разработчики как сделали, если залогинился успешно, то кидают на попап. Если залогинился не успешно, то на странице логин - показывается сообщение ошибки и тогда метод loginWith - должен вернуть мне либо return init(PopUp.class);, либо return init(LoginPage.class); или же this

Перегрузить так не выйдет, с разными возвращаемыми данными, делать два метода с разными названиями тоже фигня, делать окно попапа на пейдже LoginPage - решение мне не нравиться. Для каждого попапа я делаю отдельный класс (попапов очень мало в приложении) и заношу в тот класс элементы попапа и getText() элемента…

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

Страшного ничего нет, пока в тесте 5 строк. Страшнее становится, когда тест, к примеру, на 50+ степов (мы ведь говорим об e2e сценариях, ведь так?), но занимает он 150 строк кода, 100 из которых - никому ненужный шум из инициализаций различных объектов и прочей низкоуровневой языковой составляющей. И вот тут наступает весьма забавный момент, ведь за всем этим шумом становится сложно понять, что собственно этот тест проверяет.

Я пришел в автоматизацию из ручного тестирования, и для меня дико читать высокоуровневый тест, содержаший всякий ненужный мусор. К примеру:

LoginPage loginPage = new LoginPage(driver);
loginPage.login("name", "password");
homePage.navigateToUserAccount();
UserAccountPage userAccountPage = new UserAccountPage(driver);
Map<String, String> userInfo = userAccountPage.getUserInfo();
Assert.assertEquals(userInfo, new HashMap<>(){{...}});

Сколько мне надо потратить времени, чтобы прочитать этот текст и понять, о чем он? А сколько подобной информации вы сможете уместить в голове, прежде чем потеряете нить происходящего? Насколько легко вы сможете отделять степы от ненужного мусора? PageObject как раз и придуманы, чтобы убрать весь этот garbage из тестов, оставив лишь то, что действительно важно - DSL вашего приложения. Возьмем ваш же пример. Что такое sendKeys? Я конечно знаю, что это такое, но как этот вызов относится к тестируемому приложению? Неужели вы считаете, что это важная для теста информация?

С другой стороны, я также могу понять девелоперов, которые пришли в автоматизацию, и которым не нужна вся эта “ерунда” с пейджами и т.п. Они пишут так, как им удобно, поскольку они сами себе и разработчики, и тестировщики.

Конечно. Ведь у каждого теста свои данные. Свои юзера, подгружаемые из различных пулов, в зависимости от прав и ролей в системе. Молчу уже о двух-факторной аутентификации. Мы ведь не о псевдо-тестировании говорим, когда 1 юзер на все случаи жизни используется? Тогда конечно можно и вынести авторизацию в отдельный глобальный прекондишен.

1 лайк

Погодите. Ну попап же - не отдельная пейджа, а ее часть, так? Физически вы ведь остаетесь на LoginPage? Почему не поместить попап в форме композиции в LoginPage? Позитивные и негативные тесты у вас отдельные ведь? В случае ошибок из логин пейджи можно обращаться к объекту попапа и получать нужную информацию об ошибках, которая собственно и нужна ассертам. Зачем же возвращать весь попап наружу?

Нет, так не получится, к сожалению, PopUP - это новая страница, с урлом http://site.com/#some-popUp=auth

Я нашел вот тут - Java 8 интерфейсы под микроскопом. Part 2 - универсальный ElementsSupplier, для себя более интересное решение:

public interface PageObjectsSupplier {

    enum PageObject implements GenericPage {
        LOGIN {
            public BasePage create() {
                return new LoginPage();
            }
        },
        HOME {
            public BasePage create() {
                return new HomePage();
            }
        }
    }

    @Step("Open browser and type the following URL: {0}")
    default LoginPage loadUrl(final String url) {
        navigateTo(url);
        return loginPage();
    }

    default HomePage homePage() {
        return (HomePage) getPageObject(HOME);
    }

    default LoginPage loginPage() {
        return (LoginPage) getPageObject(LOGIN);
    }
}

с помощью него у меня получилось вот как заменить:

// Заменить вот это:
LoginPage loginPage = init(LoginPage.class);
PopUp popUp = loginPage.open("/login.html").then().loginWith(name, pass).thenGoTo(PopUp.class);
assertThat(popUp.getTitleMessages()).isEqualTo("11111!");
assertThat(popUp.getBodyMessages()).isEqualTo("22222");

// на вот это
loginPage().open("/login.html").loginWith(name, pass);
verifyTextEquals(popUp().getTitleMessages(), "11111");
verifyTextEquals(popUp().getBodyMessages(), "22222");

Столкнулся правда еще с одной трудностью, почему то у меня на элементах NullPointerException при попытке достать getText();, но думаю подебажу и найду причину…

Спасибо, вам (@ArtOfLife), огромное за ту статйку, нашел там интересные мысли в гит-репке…

А нормально ли такой подход, как в той статье, когда много объектов страниц в интерфейсе PageObjectsSupplier так сказать инициализируется? Ведь страниц подобных может быть 200 и 300, и этот интерфейс с такой реализацией разрастется очень сильно…

Читал, что в интерфейсе должно быть парочку методов всего, как правило, а не скопище…

Страшного ничего нет, пока в тесте 5 строк. Страшнее становится, когда тест, к примеру, на 50+ степов (мы ведь говорим об e2e сценариях, ведь так?), но занимает он 150 строк кода, 100 из которых - никому ненужный шум из инициализаций различных объектов и прочей низкоуровневой языковой составляющей. И вот тут наступает весьма забавный момент, ведь за всем этим шумом становится сложно понять, что собственно этот тест проверяет.

А что за человек у Вас проделывает такой сложный путь, без какого либо понятия в программировании, пытаясь прочитать и ПОНЯТЬ тест на 150 строк? В конечном итоге эти же 150 строк вы заворачиваете в кучу методов. Но если кто то и захотит пойти вглубь ваших методов, то думаю без бутылки вряд ли сможет понять что по чем.

Что такое sendKeys? Я конечно знаю, что это такое, но как этот вызов относится к тестируемому приложению? Неужели вы считаете, что это важная для теста информация?

Если сотрудник меня когда то спросит что такое сендКейс, то я думаю что будет проведена воспитательная работа :slight_smile: Я не знаю с какими людьми Вы работаете, но у нас вроде все понимают что такое сендКейс, селект из дроп даун листа, или клик на кнопку. И мануальщики и автоматизаторы.
Это именно то что делает тест читабельным для не технических людей.

homePage.navigateToUserAccount()

Что там внутри происходит? По моему тот же sendKeys, просто что бы это увидить нада жать Ф12 пару раз и там в куче врапперов это найти, что бы понять истину происходящего. Я не говорю что тесты должны быть развернуты. Я тоже делаю много вещей в подобных методах.

open(url)
.loginWith(user)
.doSmthElse();

Это конечно красиво и понятно наверное для мануальщиков, которые априори не в состоянии читать код и тем более автотесты, да и зачем?, но отдебажить такую штуку - это много стоит. Все равно что lambda или list comprehension - выглядит красиво, элегантно, но выполняеться в 10 раз медленнее и отдебажить невозможно пока не развернешь в номальные цыклы :slight_smile:

Такое дело, главное грамотно всем распоряжаться, без фанатизма. Мне мой бывший босс тоже 3 года обьяснял что нада писать понятные тесты с точки зрения чтения для не технических людей. Но за 3 года не было ни единого мануальщика, который полез бы читать автотест. Для них главное репорт (Аллура привет). Весь анализ и суппорт проделывала команда автоматизаторов, по этому и тесты мы делали и я сейчас делаю для технических людей, а не для обезьян :slight_smile:

П.С. Т.е. вы возвращаете сразу обьект после метода, и просто по ним шагаете без всякой инициализации?
Я по началу хотел все сделать статическим, что бы страницы и методы были статик, но, уже не помню почему, но не получилось.
У меня такой тест

LoginPage loginPage = new LoginPage(driver);
loginPage.login(“name”, “password”);
homePage.navigateToUserAccount();
UserAccountPage userAccountPage = new UserAccountPage(driver);
Map<String, String> userInfo = userAccountPage.getUserInfo();
Assert.assertEquals(userInfo, new HashMap<>(){{…}});

получился бы таким:

def test():
   userAccountPage = UserAccountPage()
   userAccountPage.invoke("name", "password")
   assert userAccountPage.getUserInfo() == new HashMap<>(){{...}}), 

Как по мне, не так уж и страшно и не понятно :wink:

Т.е. под property вы подразумевали нечто такое и от этого отказались?

class LoginPage(object):
    
    def __init__(self):
        self.__user = None
        self.__password = None

    @property
    def user(self):
        return self.__user

    @user.setter
    def user(self, value):
        self.__user = value

    # аналогично с password

Если так, то действительно слишком много писанины для каждого такого текстового виджета. А что если использовать те же дескрипторы, только немного по другому, например, как показано тут?
Упрощенный пример:


# сначала определеям базовый класс для виджета
class BasePageElement(object):

    # определяем сеттер
    def __set__(self, obj, value):
        """Устанавливаем текст виджета"""
        obj.driver.find_element(*self.locator).send_keys(value)

    # определяем геттер
    def __get__(self, obj, owner):
        """Получаем текст виджета"""
        element = obj.driver.find_element(*self.locator)
        return element.get_attribute("value")

# далее определяем два виджет-класса с логином и паролем
class LoginElement(BasePageElement):
    locator = (By.XPATH, '//div[@class="login"]')


class PasswordElement(BasePageElement):
    locator = (By.ID, 'password')

# а затем посредством композиции определяем их в педж обжекте
class LoginPage(BasePaget):
    """Страница входа в систему"""

    # тут описываем все атрибуты
    username = LoginElement()
    password = PasswordElement()

    # далее идут экшены страницы
    def click_enter_button(self):
        """Кнопка входа"""
        element = self.driver.find_element(*LoginPageLocators.ENTER_BUTTON)
        element.click()


# сам тест
def signin_test():
    lp = LoginPage(driver)
    lp.username = 'my_login'
    lp.password = 'my_password'
    lp.click_enter_button()

Вроде получается лаконично и красиво. Я как начинающий автоматизатор хотел пойти этим путем. Считете ли вы его оправданным?

@saw_tooth и @Oleg_Kuzovkov, на сколько я понял, вы пишете свои тесты на Python. По сабжу темы, не спрашиваю про идеальную архитектуру, не могли бы вы поделиться своей структурой проекта автотестов? Где лежат тесты, где педж обжекты, utils или классы-помощники, создавали ли вы отдельные модули/директории для фикстур? Может быть что-нибудь еще для структуры проекта посоветуете?

1 лайк

целый класс для одного элемента?

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

class BasePageElement(object):
    """Базовый класс элемента"""
    
    def __init__(self, locator):
        self.locator = locator

    def __set__(self, obj, value):
        """Устанавливает значение для элемента"""
        element = find_element(obj.driver, self.locator)
        element.send_keys(value)

    def __get__(self, obj, owner):
        """Получает значение элемента"""
        element = find_element(obj.driver, self.locator)
        return element.get_attribute('value')

# а затем посредством композиции определяем их в педж обжекте
class LoginPage(BasePaget):
    """Страница входа в систему"""

    # тут описываем все атрибуты
    username = BasePageElement([By.XPATH, '//div[@class="login"]'])
    password = BasePageElement([By.ID, 'password'])

    # далее идут экшены страницы
    def click_enter_button(self):
        """Кнопка входа"""
        element = self.driver.find_element(*LoginPageLocators.ENTER_BUTTON)
        element.click()

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

Я там писал, что это лобовой подход без reflection. С рефлексией все будет гораздо компактней. Может на днях залью новую версию.

1 лайк

Спасибо, было бы здорово, ждем :slight_smile:

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

А кто сказал, что у меня эти 150 строк будут где-то обернуты? Вы сейчас говорите о процедурном программировании. При наличии хорошо спроектированной архитектуры / ООП, разумной композиции / декомпозиции у вас не будет никаких страшных конструкций даже в пейджах. Заметьте, что есть большая разница - спихну ли я весь “мусор” в пейджи (как вы сказали - просто обернув его), либо организую правильное разделение бизнес-логики (доменный модуль) от низкоуровневой составляющей (фреймворк).

Заставить кого-то заучить Selenium API - дело нехитрое. Тут и воспитательные работы не нужны. Но зачем по-вашему тогда нужны все эти обертки - PageObjects, Frameworks и т.п., если вы все равно выносите низкоуровневые API на уровень теста? Чем ваш подход по факту отличается от лобового - когда я беру драйвер, вызываю findElement, затем - sendKeys? Почему же вы драйвер спрятали с глаз долой, а sendKeys - нет? Т.е. одни низкоуровневые API вы оборачиваете и говорите, что так правильно. Другие оставляете, и говорите - “если сотрудник этого не понимает - его надо наказать”. Где логика?

Ну, во-первых, это был пример, как писать не совсем обоснованно (в контексте вынесения пейджей на уровень тестов). Ну а в целом, по-моему, из названия все предельно понятно - перейти в аккаунт пользователя. В чем отличие от sendKeys? В том, что navigateToUserAccount - это DSL приложения. sendKeys - это API драйвера. Повторюсь, вся теория написания хороших высокоуровневых тестов основывается прежде всего на наличии domain specific language - языка вашего приложения, который использует java / c# / python и т.п. в качестве host language. Иначе вся эта затея с PageObjects и прочими обертками - пустой никому ненужны фарс для идеалистов. Нравится писать sendKeys? Дело ваше. Учить такому других под предлогом “ты что - тупой?.. как можно этого не понимать?.. это ведь очевидно!.. отправить ключи!.. ключи, Карл!” - я бы не стал.

Почему именно для мануальщиков? Любому человеку будет проще писать текст на том языке, на котором он каждый день общается. Вы приходите на работу, каждый день употребляете терминологию вашего приложения, общаетесь с командой на английском. Вам при написании тестов с использованием DSL даже не придется задумываться, что писать. Вы будете осуществлять вызовы так же, если бы смотрели на реальную страницу вместо кода. В этом и суть - разрабатывая тесты, вы думаете о тестировании, а не о программировании (о том какую структуру данных выбрать, или какой объект сейчас создать).

При вменяемом логировании, когда у вас формируются адекватные стек-трейсы, пишется видео, снимаются скриншоты, трекаются все реквесты / респонсы, вам даже не нужно ничего дебажить, т.к. репорт предоставит вам всю нужную информацию. Это факт. Бывают конечно очень экзотически кейсы, требующие дебага, но на это тратится минимум времени. Чем проще код, чем правильней спроектирована архитектура, чем меньше вы выносите на поверхность то, в чем очень просто допустить ошибку, - тем стабильней будут ваши тесты, и меньше времени придется тратить на root cause analysis.

У меня сейчас обратная ситуация. Студенты-мануальщики приходят и через 2 недели уже создают свои первые PR’s с авто-тестами. А все потому, что мы настолько изолировали низкоуровневые API, что допустить ошибку ну очень сложно. Они пишут тесты на языке приложения, что значительно снижает порог вхождения.

Но в целом, даже опытные автоматизаторы соглашаются, что писать тесты стало гораздо проще.

Ну да. PageObject pattern в чистом виде предполагает, что каждый метод будет возвращать либо текущую пейджу, либо новую. Причем, для избавления от наслаивания объектов в памяти, страницы можно кешировать.

3 лайка

Да, нечто подобное, да только подход с наследованием гет/сет нихрена не гибкий, и в большинстве случаев приходится искать всеравно по месту (какой то особый вейт, отдать компонент не только когда он его нашел, а и когда он видимый). Это реально проблемно. Нужно дополнять класс как раз этими методами, что вызывает кучу проблем, и постоянный вопросов.

Что касаемо архитектуры, вы видели когда нибудть flask-проект - это оно)) Есть модель, есть тесты, есть тулы , все лежит в трех папках, тесты поделены по папкам еще по модулям (зачастую это страницы или типа админка/юзерка), модель ледит в корне, дальше в папках педйдж обжекты (виджеты отдельно, страницы отдельно). Фикстуры в каждой папке тестируемого модуля.
как то так: Clip2Net — screen capture tool for Windows, Android, iPad, Mac, Linux

1 лайк

Интерестная конечно идея и подход.
Но есть пару моментов, которые у меня не укладываються в голове:

  1. Почему-то, вы считаете что програмировать нужно не на Джава/Питоне и т.д., а на языке приложения (DSL). Что никому ничего не нада знать, просто вызывай методы которые все за тебя делают и танцуй под музыку (что в принципе не плохо :)) Правда люди не учаться думать, и не знают как и что устроено и как оно работает. Я не думаю что человек пописав тесты тспользуя “DSL” сможет чему то научиться что бы просто поменять работу.
    Но кто то же должен создать такие методы, кто то же должен их саппортить и продумывать новые и новые сценарии. А что если у Вас не будет реализован сценарий, который я хочу использовать в тесте? Мне нужно бежать к Вам, или прописать его самостоятельно на нужной мне страницы? Я к тому, что если у Вас новый проэкт, то сколько времени Вам нужно потратить что бы начать писать первые тесты для того что бы создать нужные декларации и нужные методы для индивидуальной страницы? Для вас дико смотреть на sendKey в тестах, для меня диковина это видить тест в одну строку и что то типа “open(”/login.html").then().loginWith". Вот что такое then() здесь? Зачем оно мне нада? :slight_smile: На вкус и цвет как говориться…

  2. Я не согласен с тем что абсолютно все низкоуровневые АПИАЙ должны быть обернуты в методы на страницах, потому как есть достаточто большое количество примитивных тестов, например проверка на спец символы, длинну вводимых данных, XSS атаки, и все что связано с тестированием вводного поля. Зачем в таком случае мне создавать на странице метод sendSomethingToEmailTextField(String something)? Потом в методе loginToApplication(user, password), мне нада его же использовать? А что если у меня много вводных полей на странице? И я хочу протестировать их все с определенным сетом сценариев, что выступает у меня как Дата Соурс в тест кейс, где поле ввода это входящий параметр и я его инициализирую рефлекшеном. Как мне после этого знать какой метод мне нада вызывать из страницы для этого поля вместо банального sendKeys() что бы передать данные (Карл!!1) ?

  3. Мне не нравиться подход тестов “с головы”, где точка старта каждого теста это open(url). Постараюсь обьяснить почему: Я как то раз работал на гиганском проэкте, который длиться по сей день уже где-то 15 лет. Так вот у нас было тысячи и тысячи UI desktop and Web тестов. Окон в приложении было очень много, и порой что бы открыть нужное окно для тестирования, нужно было открыть штук 10 окон перед этим. А окошки те могли иметь достаточно крутой функционал. Так вот представьте себе что каждый из 100 (к примеру) тестов будет иметь в себе (по Вашему подходу) логику открытия этой страницы:

open(url).loginWithCredentials(user, password).navigateToWindow1().doSomething().navigateToWindow2().doSomething().navigateToWindow3.doSomething().navigateToWindow4().doSomething()…finallyWeGetToOurWindow().doSomeActions()
assert ‘bla’==‘bla’

Не думаю что тесты были бы супер практичны в таком случае. И отсюда возникает вопрос, зачем мне знать/помнить настолько досконально свое приложение, что бы в каждом тесте знать что именно мне нужно сделать что бы попасть на нужную мне страницу?
Именну тут и родилась идея “цепного открытия”, которая реализует логику открытия последством выполнения нужных действий на предыдущей страницы. Пример

class LoginPage:
    def invoke(self):
        Browser.open(url)
        
class StartPage:
    def invoke(self, user, password):
        login_page = LoginPage()
        login_page.invoke()
        login_page.login(user, password)
        
class UserPage:
    def invoke(self, user, password):
        start_page = StartPage()
        start_page.invoke(user, password)
        start_page.open_tab("user")

Таким образом для того что бы протестировать UserPage, мне нада просто вызнать метод invoke(), и мне плевать какие действия нужно сделать этой страницы что бы открыться, потому что логика вся поделена и распределена между всеми попутными “родительскими” страницами.
Разница в том, что Вы в тестах шагаете с верзу вниз, а я с низу вверх. В чем плюс - это то что каждый из invoke использует абстрактное поведение типа:

 if not self.exists():
   self.invoke()
   assert self.exists(10)

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

Мне все таки кажеться что люди должны знать фреймворк и как он работает, а не просто “писать тесты” используя ДСЛ потому что это просто и я так придумал :slight_smile: Они должны понимать что за всем этим стоит тот же банальный sendKeys and click, который они почему-то могут и не знать, но считают себя автоматизаторами.

П.С. Спасибо за дискуссию, очень интерестно и важно услышать мнение со стороны.

Это мелочи )))
Сейчас пишу на cucumber js + selenium-webdriver.
Не сразу вник в промисы, а там все на них построено.

А можно глянуть на этот “кайф”? Небольшой пример теста :slight_smile:

Вот один step - When (текст шага придумал сейчас):

    this.When('Кликаю на, допустим, кнопку "$visibleText"', function(visibleText) {
        var By = this.webdriver.By;
        var driver = this.driver;
        var locator;

        locator = By.xpath(".//*[@id='searchForm']/div[contains(., '" + visibleText + "')]");

        return this.waitFor(locator)
            .then(function(element) {
                return element.click()
            }.bind(this))
    });

сам waitFor:

    function waitFor(locator, waitTimeout) {
        waitTimeout = waitTimeout || 30000;
        var driver = this.driver;
        return driver.wait(function() {
            return driver.findElements(locator)
                .then(function(results) {
                    return results[0];
                });
        }, waitTimeout, "error timeout");
    }

Тест может быть кривым, учусь еще ). После питона.

1 лайк

Боже какая красота :slight_smile: А зачем с питона спрыгнули то?