Pull to refresh

Автоматизация с Codeception + Gherkin + PageObject для самых маленьких

Reading time8 min
Views10K

Не найдя в интернете ни одного конкретного примера реализации Gherkin с паттерном проектирования Page Object для Codeception, подумалось, что будет не лишним рассказать интернету о собственной реализации этого паттерна.

Эта статья рассчитана скорее на тех, кто уже немного знаком с Codeception или похожими фреймворками, но ещё не знает, как при помощи Page Object сделать тесты более читаемыми, упростить их поддержку и сократить объемы лишнего кода. Тем не менее, я постаралась пошагово изложить все основные моменты сборки проекта автоматизации с нуля.

Шаблон Page Object позволяет инкапсулировать работу с элементами страницы, что в свою очередь позволяет уменьшить количество кода и упростить его поддержку. Все изменения в UI легко и быстро внедряются — достаточно обновить Page Object класс, описывающий эту страницу. Ещё одно важное преимущество такого архитектурного подхода — он позволяет не загромождать тестовый сценарий HTML деталями, что делает его более понятным и простым для восприятия.

Так выглядит тест без применения Page Object



С применением Page Object



Детально останавливаться на установке базового окружения не буду, приведу свои исходные данные:

  • Ubuntu Bionic Beaver
  • PHP 7.1.19-1
  • Composer — менеджер по управлению зависимостями для PHP, установлен глобально
  • PhpStorm — среда разработки

Для запуска тестов нам также понадобится:

  • Сhromedriver 2.41 (по ссылке можно не только скачать, но и посмотреть, какой драйвер соответствует вашей версии хрома)
  • Chrome 67.0.3396.99
  • Selenium-server-standalone-3.14.0 (статья об установке selenium на ubuntu)

Разворачиваем Codeception


Приступаем к установке Codeception:

Открываем в терминале нужную нам директорию, где будем собирать проект, или создаём директорию для проекта и переходим в неё:

mkdir ProjectTutorial
cd ProjectTutorial

Устанавливаем фреймворк Codeception и его зависимости:

composer require codeception/codeception --dev



Файл установки зависимостей composer.json в проекте будет выглядеть так:

{
   "require": {
       "php": ">=5.6.0 <8.0",
       "facebook/webdriver": ">=1.1.3 <2.0",
       "behat/gherkin": "^4.4.0",
       "codeception/phpunit-wrapper": "^6.0.9|^7.0.6"
   },
   "require-dev": {
       "codeception/codeception": "^2.5",
       "codeception/base": "^2.5"
   }
}

Разворачиваем проект:

php vendor/bin/codecept bootstrap



Больше информации о способах установки проекта можно получить в официальной документации.

На этом этапе в нашем проекте создаются три набора (suite) тестов. По умолчанию Codeception разделяет их на приемочные, функциональные и юнит. К этим наборам Codeception также генерирует три yml файла. В них мы указываем все необходимые конфигурации, подключаем модули и свойства для запуска наших тестов.

Этот урок построен на примере Acceptance теста, поэтому и настройки буду приводить в Acceptance.suite.yml.

Открываем наш проект в PHP Storm (или другой любимой среде разработки) и переходим в Acceptance.suite.yml (по умолчанию он лежит в папке tests/acceptance.suite.yml).
Прописываем минимально необходимые зависимости и обязательно обращаем внимание на форматирование. Модули разделяются знаком "-" и должны располагаться на одном уровне, иначе при запуске теста посыпятся ошибки.

Получается:

actor: AcceptanceTester
modules:
    enabled:
    - WebDriver:
        url: 'http://yandex.ru/' //тут может быть любой сайт, для которого вы будете писать свой сценарий
          browser: 'chrome'
    - \Helper\Acceptance //в этом модуле будут находиться методы взаимодействия с элементами страницы

gherkin:
 contexts:
    default:
       - AcceptanceTester

И ещё немного подготовительных работ:

Создадим в корне проекта отдельную директорию (у меня lib).
В этой директории создаем исполняемый файл run.sh, который будет запускать Selenium и Chrome Driver.

Помещаем сюда же Selenium и Chrome Driver, и пишем в run.sh команду запуска:

java -jar -Dwebdriver.chrome.driver=chromedriver_241 selenium-server-standalone-3.14.0.jar

Как это выглядит в проекте:



Возвращаемся в консоль и меняем права доступа:

chmod +x ./run.sh

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

Запустить Selenium и Webdriver можно прямо сейчас, чтобы больше к этому не возвращаться. Для этого открываем новую вкладку терминала, переходим в директорию, где лежит файл run.sh и пишем команду запуска:

~/AutomationProjects/ProjectTutorial/lib$ ./run.sh

Убедились, что сервер запущен:



Оставляем его в запущенном состоянии. На этом подготовительные работы завершены.

Пишем тестовый сценарий


Переходим к созданию feature файла для нашего тестового сценария. Для этого в Codeception предусмотрена специальная команда, запускаем её в консоли:

cept g:feature acceptance check

(прим. “check” — название моего теста)

Видим в папке acceptance новый файл check.feature.



Дефолтное содержимое нам не нужно, сразу удаляем и пишем свой тест.

Для того, чтобы сборщик распознал кириллицу, не забываем начинать сценарий с #language: ru.
Пишем коротенький сценарий на русском языке. Напоминаю, что каждое предложение должно начинаться с ключевых слов: “Когда”, “Тогда”, “И”, символа “*” и др.

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



Чтобы увидеть, какие в тесте есть шаги, прогоняем в терминале свой сценарий:

cept dry-run acceptance check.feature



Шаги сценария выводятся в консоль, но их реализации ещё нет.

Тогда запускаем команду, которая автоматически сгенерирует шаблоны для реализации наших методов:

cept gherkin:snippets acceptance



Все названия из сценария, которые были в кавычках, заменяются на переменные :arg.
Копируем их из терминала и вставляем в файл AcceptanceTester.php, где будут лежать методы работы с элементами страницы.



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



Тут всё просто, но ещё проще, если вы работаете в умной среде разработки, типа Storm, которая сама будет подсказывать нужные команды из библиотеки:



Удаляем лишнее и пишем методы:

/**
* @When пользователь находится на Главной странице 
*/
public function step_beingOnMainPage($page)
{
$this->amOnPage('/');
}

/**
* @Then на странице присутствует :element
*/
public function step_seeElement($element)
{
 this->seeElement($element);
}

/**
* @Then пользователь нажимает на :button
*/
public function step_clickOnButton($button)
{
  $this->click($button);
}

/**
* @Then вводит в поле :field текст :text
*/
public function step_fillField($field, $text)
{
   $this->fillField($field, $text);
}

Посмотрим, что получилось. Запускаем команду, которая покажет нам, какие методы (step) у нас теперь имеют реализацию.

cept gherkin:steps acceptance



Успех!

Но степы в feature файле всё ещё не распознаются как методы.



Чтобы Storm понял, что делать со степами, провернем трюк с реализацией интерфейса Context из namespace Gherkin Context.

namespace Behat\Behat\Context {
    interface Context {}
}

Заворачиваем наш класс AcceptanceTester в namespace и наследуем от Context

implements \Behat\Behat\Context\Context



Теперь все степы feature файла привязаны к их реализации:



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

Тогда получим тест вида:



И можно запускать:

cept run acceptance



Passed.

прим. Если элементы страницы долго грузятся, можно добавить в нужный метод ожидание:

$this->waitForElementVisible($element);

Возвращаемся к нашему тестовому сценарию. Расстраиваемся, что вся читаемость теста теряется из-за того, что вместо понятных названий элементов и страниц мы видим HTML элементы и url.

Если мы хотим это исправить, пришло время переходить к реализации паттерна Page Object.

Переходим к Page Object


В каталоге _support создаём директорию Page, куда будем складывать наши страницы-классы:

php vendor/bin/codecept generate:pageobject MainPage

Первый Page Object — главная страница Яндекса, назовем её MainPage, класс назовем так же:



Здесь мы объявляем статические поля и методы, чтобы их можно было вызывать, не создавая объекта.

Поскольку в конфигурациях Acceptance.suite.yml мы уже указали стартовую страницу url: yandex.ru, то для главной страницы достаточно будет указать

public static $URL = '/';

Дальше идёт массив элементов страницы. Описываем локаторы для нескольких элементов и даем им понятные и уникальные названия.

Теперь необходимо добавить метод getElement, который будет возвращать локатор по названию элемента из массива.

В результате имеем:

<?php //location: tests/_support/Page/MainPage.php
namespace Page;
/** Главная страница */
class MainPage
{
   public static $URL = '/';
   public static $elements = array(
       'раздел Афиша' => "//*[@id='wd-wrapper-_afisha']",
       'гиперссылка Афиша' => "//*[@data-statlog='afisha.title.link']",
       'иконка Погода' => "//*[@class='weather__icon weather__icon_ovc']|//*[@class='weather__icon weather__icon_skc_d']", 
   );
   public static function getElement($name){
       return self::$elements[$name];
   }
}

Добавлю ещё пару классов страниц:

/** Афиша */



/** Афиша — Результаты поиска*/



Возвращаемся в класс AcceptanceTester.php, где мы писали наши методы.
Создадим в нём массив из классов PageObject, где мы присвоим страницам названия и укажем их имена классов в пространстве имен:

 private $pages = array(
            "Главная страница" => "\Page\MainPage",
            "Афиша" => "\Page\AfishaPage",
            "Афиша - Результаты поиска" => "\Page\AfishaResult"
        );

Каждый новый PageObject аналогичным образом добавляем в этот массив.

Дальше нам нужно создать поле currentPage, в котором будет храниться ссылка на PageObject текущей страницы:

private $currentPage;

Теперь напишем метод, при вызове которого мы сможем получить currentPage и инициализировать нужный нам класс PageObject.

Логично таким методом сделать степ «Когда пользователь перешел на страницу „Название страницы“. Тогда самый простой способ инициализации класса PageObject, без проверок, будет выглядеть так:

/**
* @When пользователь перешел на страницу :page
*/
public function step_beingOn($page)
{
   // Инициализируется нужный pageObject 
   $this->currentPage = $this->pages[$page];
}

Теперь пишем метод getPageElement, который позволит нам получать элемент, а точнее, его локатор с текущей страницы:

private function getPageElement($elementName)
{
   //Берет нужный элемент по его имени с нужной страницы
   $curPage = $this->currentPage;
   return $curPage::getElement($elementName);
}



Для уже реализованных методов необходимо заменить аргументы, которые в начале мы получали непосредственно из текста feature, на элементы из PageObject, то есть:

$arg

примет вид

getPageElement($arg))

Тогда наши методы примут вид:

/**
* @When пользователь находится на странице :page
*/
public function step_beingOnMainPage($page)
{
   // Открывается страница и инициализируется нужный pageObject
   $this->currentPage = $this->pages[$page];
   $curPage = $this->currentPage;
   $this->amOnPage($curPage::$URL);
}

/**
* @Then на странице присутствует :element
*/
public function step_seeElement($element)
{
   $this->waitForElementVisible($this->getPageElement($element));
   $this->seeElement($this->getPageElement($element));
}

/**
* @Then пользователь нажимает на :button
*/
public function step_clickOnButton($button)
{
   $this->click($this->getPageElement($button));
}

/**
* @Then вводит в поле :field текст :text
*/
public function step_fillField($field, $text)
{
   $this->fillField($this->getPageElement($field), $text);
}

/**
* @Then пользователь удаляет текст в поле :field
*/
public function step_deleteText($field)
{
   $this->clearField($this->getPageElement($field));
}

Добавила ещё один метод для отображения результатов поиска по нажатию клавиши Enter:

/**
* @Then нажимает на клавиатуре ENTER
*/
public function step_keyboardButton()
{
   $this->pressKey('//input',WebDriverKeys::ENTER);
}

Последний шаг — когда все необходимые методы и PageObjects описаны, нужно провести рефакторинг самого теста. Добавим шаги, которые будут инициализировать PageObject при переходе на новую страницу. У нас это “*пользователь перешел на страницу :page”.

Для наглядности допишу ещё несколько шагов. В результате получился такой тест:

#language: ru
Функционал: Поиск на Яндекс Афише
 Сценарий: Открываем главную страницу. Переходим в раздел Афиша. Ищем хорошее кино.

   Когда пользователь находится на странице "Главная страница"
   Тогда на странице присутствует "иконка Погода"
   Тогда на странице присутствует "раздел Афиша"
   И пользователь нажимает на "гиперссылка Афиша"

   Тогда пользователь перешел на страницу "Афиша"
   И вводит в поле "строка Поиска" текст "Зелёный слоник"
   И нажимает на клавиатуре ENTER

   Тогда пользователь перешел на страницу "Афиша - Результаты поиска"
   И на странице присутствует "События в ближайшие дни"
   Тогда пользователь удаляет текст в поле "строка Поиска"
   И вводит в поле "строка Поиска" текст "Головаластик"
   И нажимает на клавиатуре ENTER

Такой тестовый сценарий понятен и читаем для любого постороннего человека.

Запускаем!

Для просмотра более детального результата прогона можно воспользоваться командой

cept run acceptance --debug

Смотрим результат:



Таким образом, использование Page Object паттерна позволяет отделить все элементы страниц от тестовых сценариев и хранить их в отдельной директории.

С самим проектом можно ознакомиться по ссылке https://github.com/Remneva/ProjectTutorial

Как начинающий автоматизатор, буду признательна, если и вы поделитесь своими идеями и, возможно, подскажете как можно логичнее преобразовать и упростить структуру проекта.
Tags:
Hubs:
Total votes 12: ↑10 and ↓2+8
Comments13

Articles