Selenium + TestNG + HTMLElement. Структура фреймворка.

Добрый день,

Много не ругайтесь, вопросов много. В первую очередь по поводу структуры фреймворка.
Итак, связка: Selenium + TestNG + HTMLElements (понравилась идея разбития на блоки и типизация). (Только, пожалуйста, особо к технологиям не привязывайтесь, т.к. тот же HTMLElement можно убрать)

Примерная структура страницы приведена на рисунке.

Есть хедер, на котором находится изображение, под ним текст. Слева от них меню.
Сама страница разбивается на три блоке - Left, Content, Right.
В Left и Right находятся меню (так же допускаются другие элементы, это не столь важно)
В Content - сам контент. Варьируется от текстов до форм и пр. содержания.

Изначально я думал разбить страницу на блоки. Соотвественно - HeaderBlock, LeftBlock, RightBlock, ContentBlock.
А дальше все это выдать в классе PortalPage.

В итоге, пришел к выводу, что ни черта не понимаю, как конкретно работать с PageObject Pattern.

Поэтому первый вопрос:

  1. Что мы конкретно реализовываем в блоках?
    Возвращаем элементы геттерами? И никаких действий. Или наоборот, определенный набор действий, который мы возвращаем классу PortalPage?

Возьмем за пример Хедер.

@Block(@FindBy(id = "headerBlock")
public class HeaderBlock extends HTMLElement{
	
	//Сами элементы
	
	@FindBy(id = "image")
	Image image;
	
	@FindBy(id = "headerText")
	TextBlock headerText;
	
	@FindBy(id = "headerMenu")
	List<WebElement> headerMenuList;
	
	//геттеры
	public List<WebElement> getHeaderMenuList(){
		return headerMenuList;
	}
	
	public TextBlock getHeaderText(){
		return headerText;
	}
	
	public Image getImage(){
		return image
	}
	
	//Методы для обработки
	
	public boolean isImageVisible(){
		Utils.isImageVisible(image); // многие методы для работы с часто встречающимися элементами выведенны в отдельный класс. 
	}
}

Дальше, допустим, идет PortalPage class


public class PortalPage{

	protected WebDriver driver;
	private HeaderBlock headerBlock;
	
	public PortalPage(WebDriver driver){
		PageFactory.initElements(new HtmlElementDecorator(driver), this);
		this.driver = driver;
	}
	
	public boolean isHeaderImageVisible(){
		return headerBlock.isImageVisible();
	}
}

ну и тест

public class TestPortal(){
	PortalPage portal;

	@BeforeTest
	public void PseudoSetUp(){
		portal = new PortalPage(driver)
	}
	
	@Test
	public void pseudoTest(){
		Assert.assertTrue(portal.isHeaderImageVisible, "true");
	}
}

Т.е. вопрос, где я должен был реализовать определения, видно изображение или нет?
Если так, как указано выше, то не является ли дублирование указание этого метода в классе PortalPage?

  1. Второй вопрос - Все блоки кроме content - статические. Т.е. их содержание не изменяется (разве что меню в зависимости от страницы подсвечивается).
    Но вот сам блок контент содержит в себе абсолютно не зависимую друг от друга информацию, формы и т.д.
    Как это реализовать? (естественно, в теории :)) Писать под каждую страницу отдельный блок? т.е. Page_1_Block, Page_2_Block.
    Но как их тогда инициализировать? Не буду же я в классе PortalPage инициализировать все возможные блоки? Получится еще то полотно.

  2. Подскажите, как вообще лучше реализовать именно структуру фреймворка.

На сегодня у меня примерно так, но в области автоматизации ГУИ я абсолютный новичок

ru.qa.projetname
configuration - пэкедж с классами, для конфигурации
common - пэкедж для общих проблем
constants - константы
utils - утилиты, вроде работы с файлами и т.д.
testObject - тестовый объект
portal
pages - страницы (прим. PortalPage)
blocks - блоки (прим. HeaderBlock)
utils - утилиты для работы со списками элементов, таблицами и т.д
constants - константы, признаюсь честно, на сегодняшний день я храню здесь локаторы, т.к. пока что не придумал, где их хранить удобнее.

Ну и по большому счету все (что требуется на текущий момент), сами тесты находятся в Main/src/test

  1. Все ассерты, как я понимаю, надо писать в теле тестов?

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

Похожие темы я искал, но к сожалению, не нашел.
Спасибо!

С уважением,

Картинку вставлять не стал, т.к. получилась уж очень большая, но вот ссылка на нее.

@ArtOfLife: перезалил картинку и отформатировал код.

  1. Картинки не видно .
  2. PageOb - предполагает одна страница = один класс page . прописаны webelement FindBy
    для данной page . Ну и методы что делаеться на странице .
    Если внутри page какие-то изменяющиеся части (т.е click и что-то меняеться ) то можно добавить типа так
    private AdvancedSearchContainer advancedSearchContainer;
    private DocumentSearchResultsScreen documentSearchResultsScreen;
    инициализировать например в конструкторе .
  3. предполагает использование FindBy и PageFactory.initElements(webDriver, this) .

Все ассерты, как я понимаю, надо писать в теле тестов? - очень желательно . Поддерживать легче .

1 лайк
  1. В блоках рационально реализовывать методы для взаимодействия с блоком. Яркие представители таких вот блоков это непосредственно сами типизированные HtmlElement-ы например Select…
    Очень удобно получается, когда на странице много одинаковых блоков, реализовать такой блок и много раз использовать его функциональность например если на странице много формочек с календарями для разных пользователей (только не забывай для каждого такого при его объявлении его искать через @FindBy или в особо грустных случаях инициализировать через конструктор).

  2. Сам по себе PageObject абстракция и ты волен делать так, как удобно в твоем приложении:
    2.1 Решить что у тебя одностраничное приложение (видимо так и есть) и на одной этой портальной странице инициировать хедер, правое и левое меню и набор блоков с контентом.
    2.2 Представить что если у тебя меняется блок контент то у тебя “открывается” новая страница и сделать несколько классов Page для каждого такого блока контента, причем на каждой такой странице инициировать хедер, правое и левое меню.

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

  1. Не так существенно…может быть что-то типа:
    .configuration
    .common (сюда лучше добавить родительскую пейджу, тест итд…)
    .helpers (хелперы - без них тяжко, но по сути делят функционал с утилем)
    .utils (см. выше)
    .portal (если это приложение и у тебя во фрейме оно не одно то в нем должно быть следующее)
    .portal.pages
    .portal.blocks
    .portal.constants (делай как хочешь, но не всегда удобно делать отдельные контейнеры для локаторов, а вот всевозможные текстовые константы, бизнес значения вполне сюда пойдут)
    .portal.testobject (если тут бизнес действия с проверками, которые ты часто переиспользуешь непосредственно в тестах)
1 лайк

Я бы предложил следующую структуру:

  • BasePage с оберткой над основными экшенами по типу click, getText, select, setText и т.п. (уровень фреймворка).
  • Блоки header / menu / footer - отдельные классы, с зашитой логикой обработки сугубо своих собственных элементов. Т.е. геттеры возвращают либо состояния элементов, либо текст, используя BasePage обертки (доменный уровень).
  • PortalPage extends BasePage и посредством композиции объединяет все блоки + геттеры предоставляющие доступ к соответствующим блокам.
  • Все остальные пейджи из content секции extends PortalPage.

Итого, BasePage - прародитель всего вокруг, а PortalPage объединяет в себе все блоки и является родителем всех будущих пейджей с контентом. Драйвер, при этом, не должен быть частью доменной логики, его не нужно хранить на уровне пейджей. Поместите его на уровень фреймворка в BasePage. В общем, я бы посоветовал создать 2 модуля: фреймворк и доменная составляющая, ибо фрейм должен быть реюзабелен. А там уж сами разберетесь, куда какие из выше перечисленных компонентов распихать.

Asserts должны быть всегда в тестах. Либо в любой другой абстракции, отличной от пейджей (если вы к примеру решили написать generic verification модуль).

2 лайка

Спасибо!

Разрешите тогда еще пару уточнений.
BasePage - абстрактный класс? PortalPage extends BasePage, да?

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

Еще раз Спасибо!

Да - абстрактный. И да, PortalPage должен экстендить BasePage, т.к. в процессе последующего наследования потомкам нужен будет доступ к методам-оберткам BasePage.

Локаторы обычно хранят прямо в пейджах. Т.к. вы их декларируете на уровне instance vars, затраты на возможные изменения - минимальны. А получение состояний / текста реализуется в виде геттеров.

1 лайк

А вот еще такой вопрос.

У меня есть PortalPage, в которой инициализируются блоки, например, HeaderBlock.
В HeaderBlock есть различные методы, относящиеся только к нему.

Как лучше/принято инициализировать блоки - как публичные классы или приват/протектед?
Дабы получить в тесте что-то вроде

PortalPage.HeaderBlock.SomeAction()

либо

PortalPage.actionWrapper()

где

actionWrapper(){
HeaderBlock.someAction()
}

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

Композицией декларируете приватные блоки, а потом через геттеры достаете все внутренности. Раз уж вы используете наследование, то о какой статике может идти речь?

Ваш класс PortalPage будет родителем для всех пейджей. Дублирование методов тут в принципе невозможно. Ибо это уже будет являться переопределением.

1 лайк

Я прошу прощения, видимо запутался.

PortalPage extends BasePage, и все блоки тоже extends BasePage, а не PortalPage?

А зачем блокам расширять PortalPage, в котором вы их определяете посредством композиции? Блоки - по сути такие же “пейджи” со своими собственными элементами и экшенами. Т.е. они тоже должны иметь доступ к прародителю всех пейджей - BasePage. PortalPage - всего лишь агрегатор блоков. Но ввиду того, что он является константным для всех пейджей, то его резонней сделать родителем. BasePage же будет сокрывать всю техническую часть, отделив ее от доменной логики.

1 лайк

Просто, я же использую HTMLElements, как вспомогательный фреймворк. Поэтому BasePage будет содержать минимальный набор методов/оберток, т.к. большая часть уже реализована в HTMLElements.

Я лишь привел вам один из способов реализации, используя чистый PageObject pattern, где ключевым элементом является именно абстрактная страница. HTMLElements - это более низкий уровень абстракции - PageElement.

1 лайк

Да, извините, как-то ушел от старта темы.

Спасибо!

Привет всем! Подниму старую тему. В последних версиях HtmlElements аннотация @Block не доступна, не подскажете что с “блоками” стало?