Page Object Model + Webdriver. Пример реализации на одном тесте

в 13:01, , рубрики: automation testing, page object, webdriver, тестирование, метки: , ,

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

Примеры кода будут на C# + NUnit.

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

Почему же он так эффективен?

Наверное потому, что он вносит этот бесценный порядок в структуру проекта. Следуя принципам этого подхода, мы создаем структуру с четко разграниченными логическими модулями. А каждый такой модуль будет являться отражением логических модулей тестируемого приложения.
Это значительно облегчает поиск и переиспользование методов и, как следствие, облегчает обслуживание проекта. Также, в значительной мере сокращает время, необходимое новому участнику, для «вливания» в проект.

Структура

Примером послужит один несложный тест – логин на Facebook’e (успешностью входа будем считать появившееся имя пользователя в правом верхнем углу странички).

Пример реализации этого проекта можно скачать здесь github.com/DmitryRoss/Page-Object-Model-Article.

— Во главе всего проекта стоят классы с NUnit тестами. Для нашего теста мы сделаем один такой класс и назовем его LoginTests. В нем создадим тестовый метод с названием AssertLogin().

 [TestFixture]
    public class LoginTests : BaseTests
    {
        static LoginHelper loginHelper = new LoginHelper();
        [Test]
        public static void AssertLogin(){...}
    }

— Ниже находятся классы-Helpers. Каждый такой класс обслуживает один конкретный класс с тестами.
Но иногда, некоторые методы из классов-Helpers нужны другим классам с тестами или другим классам-Helpers. В таком случае, целесообразно вынести такие методы в отдельные общие классы, которые в иерархии структуры находятся на том же уровне, что и классы-Helpers. Ярким примером такого класса является класс навигации по приложении. Ведь переход к той или иной странице может понадобиться в разных классах с тестами или классах-Helpers. Обычно в каждом проекте есть и другие классы-Helpers, которые выносятся как общие.
Для удобства, методы возращают екземпляр класса-Helper, в котором они находятся. Это позволяет обращаться к ним в таком сокращенном виде, через точку —

 loginHelper.
                DoLogin(userName, password).
                AssertUserName(displayedUserName);

В нашем примере мы создадим один такой класс и назовем его LoginHelper.
В нем мы создадим 2 метода.
1. DoLogin(string userName, string password)
2. AssertUserName(string userName)

public class LoginHelper :TestFramework
    {
        public LoginHelper DoLogin(string userName, string password){...}
        public LoginHelper AssertUserName(string userName){...}
    } 

— И вот здесь начинается ядро нашего проекта — Pages, т.е. странички. Это классы со страницами нашего приложения. Т.е. каждый такой класс соответствует определенной странице приложения и он содержит локаторы и веб элементы этой страницы. Кроме этого, у него есть элементарные методы, которые используются на этой страничке классами-Helpers.
Я считаю, что хорошей практикой является разбиение на максимально мелкие элементы. Например, если у вас на страничке появляется всплывающее окошко, то лучше создать для этого окошка отдельный класс. Любое дробление должно быть очивидным и являться отражением чего-то в самом приложении.
Также описываемый в данной статье подход предполагает, что каждый метод, по возможности, должен возвращать экземпляр странички, на которую он приведет. Например, клик по кнопке Логин переводит нас на главную страницу. Следовательно, метод клика по кнопке Логин, возвращает экземпляр класса, в котором описана главная страничка. Если же действия в методе не предполагают переход куда-либо, то он возвращает екземпляр класса странички, в котором расположен.
Это позволяет вести удобное обращение к методам странички —

loginPage.
                TypeUserName(userName).
                TypePassword(password).
                ClickLoginButton();

Очевидно, что метод TypeUserName не приводит к переходу на какую-нибудь другую страницу, поэтому этот метод возвращает нам экземпляр страницы LoginPage(в котором сам и расположен). Тоже самое касается и метода — TypePassword. А вот метод ClickLoginButton производит переход на главную страницу, поэтому он будет возвращать экземпляр класса LandingPage. Если бы нам нужно было вызвать дальше какой-нибудь метод из главной страницы(LandingPage), то мы всего лишь поставили бы точку (.) и сразу получили бы доступ ко всем методам главной странички.
Вернемся к примеру. У нас есть две странички — страница, из которой мы Логинимся в систему и страница, на которую мы попадаем после Логина.
Создадим для них классы LoginPage и LandingPage.
На странице, с которой мы входим в систему, мы соверщаем 3 логически понятных действия (в рамках нашего теста) — заполняем поле с именем пользователя, заполняем поле с паролем и кликаем по кнопке войти.
На странице, куда мы попадаем, мы совершаем только одно действие (в рамках нашего теста) — мы убеждаемся, что имя пользователя отобразилось правильно в правом верхнем углу страницы.

public class LoginPage : TestFramework
    {
        [FindsBy(How = How.XPath, Using = USER_NAME_TEXT_FIELD)]
        public IWebElement userNameTextField;
        [FindsBy(How = How.XPath, Using = PASSWORD_TEXT_FIELD)]
        public IWebElement passwordTextField;
        [FindsBy(How = How.XPath, Using = LOGIN_BUTTON)]
        public IWebElement loginButton;

        public static LoginPage GetLoginPage(){...}
        public LoginPage TypeUserName(string userName){...}
        public LoginPage TypePassword(string password){...}
        public LandingPage ClickLoginButton(){...}

        public const string USER_NAME_TEXT_FIELD = "//input[@id='email']";
        public const string PASSWORD_TEXT_FIELD = "//input[@id='pass']";
        public const string LOGIN_BUTTON = "//label[@id='loginbutton']/input";
    }

В приведенном выше примере есть метод GetLoginPage(). Он возвращает экземпляр класса данной странички с проинициализированными элементами.

Также хотелось бы выделить некоторое удобство использования наследования у страничек. Например в большинстве веб-приложений верхняя часть (Header) отображается на всех страничках. В таком случае очень удобно описать Header в отдельном классе, а все остальные странички сделать наследниками этого класса. Тогда методы Header-а будут доступны с любой странички.
Еще наследование можно использовать тогда, когда клик по какому-нибудь чекбоксу приводит к появлению целой формы. Исходя из пинципа максмального доробления, такую форму лучше вынести в отдельный класс. Но при этом все элементы самой странички остались видны. Здесь целесообразно сделать форму наследником странички, чтобы элементы и методы странички были ей доступны.

Я часто видел, что проекты строятся без прослойки с классами-Helpers. Тесты напрямую обращались к методам страничек.
Такой подход имеет следующий недостаток. В любом проекте по автоматизации нужны методы, которые объеденяют в себя элементарные действия нескольких страничек. В нашем примере есть метод, DoLogin(). Я не до конца описал его. Если бы я написал его правильно, то в начале нужно было бы убедиться, что никто не залогинен, выйти, если залогинен и перейти на страницу, с которой входим. Далее шел бы наш метод и, после его окончания, нужно было бы убедиться, что главная страница загрузилась. Я не реализовывал это для простоты передачи основ описываемого подхода.
Но, если бы мы положили этот метод в класс странички LoginPage(как в проектах без прослойки с классами-Helpers), то мы бы потеряли эту четкую границу между логическими модулями. Ведь начинается и заканчивается процедура входа в систему не на страничке для входа.
А вот представтье себе ситуацию, когда вам нужны еще методы проверки валидации полей для входа в систему. В таком случае, класс LoginPage обрастет еще кучей методов, которые будут нужны только классу LoginTests.
Такое избыточное количество сложных методов в классе-страничке, с одной стороны, затруднят поиск элементарных методов, а с другой стороны, делают размытой грань между логическими модулями.

Но создание классов-Helpers имеет один подводный камень. Что делать с методами, которые используются в 1 класс-тесте понятно – они будут лежать в классами-Helper этого теста. Но что делать с другими сложными методами, которые могут быть использованы и другими классами-тестами?
Здесь я поступаю следующим образом – выделяю отдельные классы со сложными методами (т.е. методы, которые используют несколько элементарных методов из одного или нескольких классов-страничек.), давайте назовем их классы-Utils. Названия будут иметь вид типа NavigationUtils. Из названия класса понятно, что в нем находятся методы по навигации в приложении. Или LoginUtils – методы по логину, которые могут быть использованы другими классами, помимо класса с тестами по входу в систему. Если вам нужно создавать какой-то контент в приложении для осуществления разных тестов, то вы можете создать класс ContentUtils.
Мое правило простое — если этот метод из одного классами-Helper понадобился еще какому-нибудь классу с тестами или классами-Helper, то это метод мигрирует в соответствующий класс-Utils. Если такой класс-Utils еще не был создан, то я создаю его.
Такой подход несколько усложнит проект дополнительной прослойкой, но он поставит четкую грань между логическими модулями. Потому что в классах-страничках будут расположены только самые элементарные методы, которые могут быть выполнены только на этой страничке.

Еще немного про обслуживание.

В проектах, где я участвовал, львиная доля изменений в приложении от билда к билду приходилось на локаторы. Еще небольшая доля приходилась на способы воздействия на элементы (например то, что раньше отрабатывалось обычной командой Click(), после выхода нового билда уже требовало серию команд из MouseMove(), MouseDown и MouseUp()). В таких случаях все поддержка сводится к изменению классов-страничек. А, это, как вы видите, самые переиспользуемые классы. Следовательно и усилия на поддержку минимальны.
Бывают случаи, когда в новом билде поменялаясь какая-то логическая цепочка действий, например, ссылка для навигации к какой-нибудь сущности в приложении перенесена из одного меню в другое. Тогда поддержка уже затронет классы-Helpers и классы-Utils.
И крайне редко, когда меняется логика самого теста, вам нужно будет поддерживать классы с тестами.

Идеальная структура.

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

Это набор простых правил:
1. Классы с тестами используют только классы-Helpers и классы-Utils. Не используют классы-странички, а, тем более методы фреймворка, которые работают с Selenium и Webdriver. Остальные методы фреймворка использовать можно.
2. Классы-Helpers используют только классы-странички и классы-Utils. Не используют методы фреймворка, которые работают с Selenium и Webdriver. Остальные методы фреймворка использовать можно.
3. Классы-Utils используют только классы-странички. Не используют классы-Helpers и методы фреймворка, которые работают с Selenium и Webdriver. Остальные методы фреймворка использовать можно.
4. Классы-странички используют только методы фреймворка. Не используют методы Selenium и Webdriver напрямую.
Соблюдение таких правил изначально, думаю, поможет избежать болезненных проблем с проектом в будущем и сделает поддержку максимально легкой.

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

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

Автор: DRoss


  1. yashaka:

    Такой вопрос…
    А что если ClickLoginButton() в случае неверного логина возвращает на LoginPage снова? Как тогда реализовать такой хелпер метод, что ему возвращать?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js