среда, 14 июля 2010 г.

PageObjects pattern или работаем по науке

Хочется поговорить об автоматизированных тестах, а точнее даже об организации фреймворков для них, а если быть более точным об одном из самых распространенных подходов в автоматизации тестирования через GUI - о PageObjects pattern.

Мое первое знакомство с ним состоялось 5 лет назад, когда ничего не подозревая, мой коллега предложил, организовать наш тестовый фреймворк таким образом, чтобы каждой странице соответствовал один класс, тогда мы называли его парсер (Page Parser). Идея прошла свое крещение и была воплощена, а самое главное, живет и до сих пор.

В двух словах про реализацию.


Есть абстрактный супер-класс Page, реализующий основные методы одинаковые для всех страниц тестируемого приложения (например logout), а также абстрактные методы инициализации и сбора необходимых данных со страницы, вызывающиеся из конструктора.

public abstract class Page {
protected Page() {
   init();
   parsePage();
}
protected abstract void init();
protected abstract void parsePage();

// ....
// service methods...
// ....

}

Далее, все классы реальных страниц наследуют супер-класс Page, реализует абстрактные и добавляет свои методы для функций выполняющихся на этой конкретной странице, причем методы взаимодействия с элементами управления, которые не содержат логики описываем как private void, методы выполняющие действия пользователя, вызывающие открытие новой или обновление активной страницы - public с возвращением ожидаемого объекта (public HomePage).

В итоге, при создании объекта страницы сначала она будет проинициализирована, а потом разобрана (parsed), что даст нам гарантию того, что открыта правильная страница и все необходимые данные собраны, т.е. объект готов к использованию.

Пример:
Страница Login Portal состоит из двух полей ввода User и Password, кнопки Login.

Как будет выглядеть наш PageObject?

public class LoginPortalPage extends Page {

protected LoginPortalPage() {
   super();
}

private void setUserName(String userName) {
  // код для заполнения поля Username
}

private void setPassword(String password) {
  // код для заполнения поля Password
}

private void pushLoginButton() {
  // код для нажатия на кнопку Login
}

protected void parsePage() {
  // Разбор элементов страницы
  // Заполнение необходимых переменных данными со страницы
}

protected void init() {
  // Инициализация страницы
  // Проверка корректности загрузки
}
}

Из данного примера видно, что методы в данном классе LoginPortalPage соответствуют тем действиям, которые может делать пользователь на этой странице, а именно ввести имя (setUserName), пароль (setPassword), нажать кнопку Login (pushLogin).

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

private void loginAs(String userName, String password) {
    setUserName(userName);
    setPassword(password);
    pushLoginButton();
}

public HomePage login(String userName, String password) {
    loginAs(userName, password);
    return new HomePage();
}

public ErrorLoginPage loginInvalid(String userName, String password) {
    loginAs(userName, password);
    return new ErrorLoginPage();
}


Таким образом в своем “окончательном”  варианте класс LoginPortalPage будет выглядеть следующим образом:

public class LoginPortalPage extends Page {

protected LoginPortalPage() {
   super();
}

private void setUserName(String userName) {
  // код для заполнения поля Username
}

private void setPassword(String password) {
  // код для заполнения поля Password
}

private void pushLoginButton() {
  // код для нажатия на кнопку Login
}

protected void parsePage() {
  // Разбор элементов страницы
  // Заполнение необходимых переменных данными со страницы
}

protected void init() {
  // Инициализация страницы
  // Проверка корректности загрузки
}

private void loginAs(String userName, String password) {
    setUserName(userName);
    setPassword(password);
    pushLoginButton();
}

public HomePage login(String userName, String password) {
    loginAs(userName, password);
    return new HomePage();
}

public ErrorLoginPage loginInvalid(String userName, String password) {
    loginAs(userName, password);
    return new ErrorLoginPage();
}
}
 

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

5 комментариев:

St. Mark комментирует...

Использую нечто похожее, со следующими отличиями:

1. Класс назначается не странице, а логическому блоку, все элементы которого расположены на одной странице. Например head и bottom всех страниц не сложного сайта, как правило одни и те же.

2. В поля такого класса добавляю объекты - метаданные(например xpath, css, id, name, description, etc.)

3. Собственно парсинг, а точнее перебор, происходит по таким полям, реализуемый рефлекшеном в методе parent-класса.

4. Для тестирования стандартных функциональностей (поиск, хлебные крошки, пейджинг и т.д.) сделал отдельный класс, общий для всех проектов. Получается что-то типа такого: TestActions.fulltxtsearch();

P.S. Несколько месяцев по такой схеме пишу тесты, так что возможно это и не особо эффективно, но пока работает на ура.

Алексей Булат комментирует...

Спасибо, за комментарий.
Было бы интересно посмотреть пример подобного класса, скажем относительно аналогичной страницы Логин из примера в тексте поста.

P.S. сейчас готовлю еще несколько статей+примеров использования PajeObject паттерна, где постараюсь описать работу с конкретными инструментами, ресурсами (xpath, css, id, name, description, etc.) и т.д. Главное чтобы хватило сил и терпения :)

St. Mark комментирует...

Класс объекта на странице:

public class Node
{
public string Xpath;
public string Css;
public string Description;
/*далее другие поля*/
/*методы пока отсутствуют*/
}


Класс блока:

public class Block
{
public void Scanref ()
{
/*код реализующий "пробегание" по полям класса*/
}

public bool IsThisPage ()
{
/*код, который определяет верный ли блок на странице*/
/*определение происходит по наличию все объектов, доп. данных (соседние объекты)*/
}
/*далее идут другие менее интересные методы*/
}


Пример наследуемого класса:

public class Header : Block
{
public static Node Banner2 =
new Node
{
Xpath = "",
Css = "css=*>td.banners-col>div.content-block>div.content-item+div.content-item",
Description = "Второй баннер сверху"
};
}


Пример использования в тесте:

[Test]
public void TheNewsIndexSmokeTest()
{
_selenium.Open( "index.html" );
Assert.IsTrue( _selenium.IsElementPresent( Index.Banner2.Css ) );
Console.WriteLine( Index.Banner2.Description + " найден" );
}

Алексей Булат комментирует...

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

Алексей Булат комментирует...

По какой-то причине последний комментарий не прошел :( поэтому дублирую его сам:
-------------------
St. Mark прокомментировал(а) ваше сообщение "PageObjects pattern или работаем по науке":

Прошу прощения за код, табы почему-то после публикации удалились.
Будет очень интересно взглянуть на Ваш вариант реализации хранения данных. Сам я выносить в отдельные файлы мета-данные пробовал, но столкнулся со следующими проблемами:

1. Никак не получалось сделать динамическим доступ к полям получавшегося класса в IDE (проще говоря, что бы после точки вылезал список с вариантами, ибо не всегда быстро вспоминаешь названия десятка элементов на странице)

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

Условия копирования публикаций:

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