Приёмочное тестирование (Acceptance testing) web-приложений с PHP

Перевод статьи padraic, с сайта ZEND Developer Zone. Статья была написана в середине 2007 года и некоторые ссылки на версии и пакеты могут быть не действительны. Примечания по поводу версий я добавлю прямо в тексте.

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

Так же в некоторых случаях, когда трудно подобрать синоним, либо одно слово объясняется предложением, используется принятый за стандартный жаргон, например: логин – как сущность, так и как действие.

Приёмочное тестирование (Acceptance testing) web-приложений с PHP

Введение

В этой статье я расскажу о приёмочном тестировании (также известного как – функциональное тестирование), кое-что большинство PHP программистов смогут использовать в своей повседненвной практике. Я уверен, что многие из нас хорошо знакомы с юнит тестированием (unit testing), и даже интеграционным тестированием (Integration Testing), где же эта «лишняя спица в колесе» появляется при тестировании веб-приложений при нашей растущей одержимости Web 2.0 и AJAX, и как она отличается от первых двух методов? Ниже я объясню это. Также я покажу, как сделать приёмочное испытание с использованием убийственного сочетания PHPUnit и Selenium.

Зачем нужны приёмочные испытания?

Приёмочные испытания позволяют определить работает ли приложение так, как ожидает клиент (тот, кто заказал приложение). Теперь о том, что это означает…

Иногда трудно понять, как использовать приёмочные тесты при тестировании. Некоторые люди видят их подозрительно похожими (если не идентичными) на интеграционное тестирование. Интеграционное тестирование другой уровень процесса тестирования, отличный от юнит тестирования, которое проверяет, что компоненты приложения работают так, как ожидалось. Как и в случае с юнит тестами, это тестирование, как правило, занимается проверкой мест изолированных от остального кода с помощью MOCK объектов, заглушек и специальных тестовых классов.

Главное отличие приёмочных тестов в том, что они довольно просты. Интеграционные тесты работают по аналогии с юнит тестами, но с целыми группами связанных классов. Акцент делается на концепции «группы». Мы не тестируем каждый класс в отдельности, как с юнит тестами, а тестируем группы классов, которые вместе создают желаемый результат. Интеграционные тесты, поэтому написаны для и пишутся самими программистами. С другой стороны, приёмочные тесты работают для всего приложения (без изоляции классов и компонентов), как правило, по отношению к интерфейсу пользователя (прим. в данном случае имеется ввиду не только графический интерфейс, но и любой другой реализованный в виде запросов и видимых ответов), не важно для браузеров или веб-служб. Они пишутся для обеспечения соответствия приложения целям определенных клиентом. По сути, клиент даже может отвечать за написание тестов!

Теперь мы знаем, что приёмочное тестирование является самостоятельной практикой, но зачем нам её использовать?

  1. Она отражает ожидания клиента (а не для разработчиков!);
  2. Она оценивает, когда функциональность значимая для клиента закончена (знание о том, когда нужно остановиться);
  3. Она гарантирует быстрое определение будущего поведения, которое отличается от ожидаемого (регрессионное тестирование);
  4. Как любой хороший набор тестов она поддерживает рефакторинг в том же порядке, как юнит и интеграционные тесты.

О пользовательских историях (User Stories) и приёмочных тестах

Для тех, кто практикует экстремальное программирование, приёмочные тесты обычно пишутся для того, чтобы установить, что соответствие “пользовательским историям” является полным и не отклоняется от них в течение разработки.

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

Давайте кратко посмотрим на написание пользовательских историй.

Клиент может авторизоваться на сайте.

Как PHP разработчики некоторые из нас могут посчитать это тупым и очевидным требованием. Старайтесь не думать так. Для клиента это важная функция. Если предположить, что дальнейшие обсуждения с клиентом не вызовут никаких изменений в этой пользовательской истории, то мы можем написать приёмочные тесты еще до начала разработки. Этот процесс звучит похоже на юнит тестирование – сначала тесты, потом код (прим. На самом деле не совсем так и тут сказывается то, что автор больше работал в условиях XP или TDD, в условиях многомодульных проектов следование такому правилу себе дороже). Вы можете оценить успешность реализации, независимо от того, все ли предварительно написанные тесты успешно пройдены (в нашем случае для веб-интерфейса). Дальше, после обсуждения идей с программистами клиент написал следующие тесты.

  1. Страница авторизации отображает форму для входа;
  2. Отправка правильного сочетания идентификатора пользователя и пароля приводит к успешной авторизации;
  3. Отправка недействительной пары логин – пароль, показать регистрационную форму с указанием ошибок;
  4. Форма для логина всегда сопровождается гиперссылкой “Забыли логин или пароль?”.

Неплохо выглядит, не так ли? Эта четверка тестов уже вторая пользовательская история, дополнительные ценные знания о важной функциональности для клиента.

Клиент может восстановить забытый логин и пароль

Если каждый из четырех тестов проходит, мы можем считать, что обе пользовательских истории реализованы. Кроме того, при приёмочных тестах это явный сигнал, что пора прекратить разработку для этой функциональности. Если только клиент не придёт с новыми идеями (или уточнением существующих) нет причин расходовать ресурсы на неё. Это сделано! Вперёд!

Итерации

В нашей команде, несущейся в авангарде экстремального программирования, мы также занимаемся привязыванием пользовательских историй к конкретным “Итерациям (Iterations)”. Итерация, в общем смысле, это определенный период разработки, к концу которого у нас должна быть полностью протестированная, рабочая (хотя и неполная) версия приложения, которая будет проходить все приёмочные тесты для всех пользовательских историй, привязанных к этой итерации. Я уверен, что многие из вас сталкивались с итерационной разработкой. Итерация, как правило, длится не более нескольких недель. Учитывая, что проект может длиться несколько месяцев в плане разработки будет множество итераций, и каждая новая планируется на основе предыдущей.
Возвращаясь к нашему примеру. Пользовательская история про авторизацию была записана в итерацию # 1. Уверенный, что пришло время для разработки, вы загружаете ваш любимый редактор и принимаетесь за воплощение идеи. Следующий шаг? Написать приёмочные тесты в виде автоматизированного тестирования там, где это возможно.

Подготовка к приёмочному тестированию

Требования:

Многие PHP программисты стремятся использовать PHP библиотеки для написания приёмочных тестов, и это подход я использую здесь. В этой статье мы будем использовать PHPUnit, который с версии 3.0 содержит класс PHPUnit_Extensions_SeleniumTestCase, который может быть использован для определения приёмочных тестов и используется с Selenium Core при тестировании. Важно отметить, что эти тесты не являются юнит тестами. Да, PHPUnit это библиотеки для юнит тестирования, но они столь же эффективны для интеграционных и приёмочных тестов.

Руководство по использованию PHPUnit с Selenium можно почитать тут: http://www.phpunit.de/pocket_guide/3.1/en/selenium.html. Расширение PHPUnit для Selenium также потребует, установленного пакета PEAR Testing_Selenium. PHPUnit и Testing_Selenium могут быть взяты из PEAR (PHPUnit 3 доступен только через phpunit.de, руководство по установке тут http://www.phpunit.de/pocket_guide/3.1/).

Selenium это веб-приложение для приёмочного тестирования ( прим. не совсем верно. Selenium это всё-таки функциональное тестирование, но может быть использован и для приёмочного) веб-приложений, созданное ThoughtWorks. В него входит несколько пакетов приложений, включая Selenium Core, Selenium RC и Selenium IDE. Его цель состоит в том, чтобы запустить тесты в реальном браузере (со всеми тараканами и примочками!), используя Selenium Core для воспроизведения действия пользователя, их проверки и создания отчетов с результатами тестирования. Selenium Core написан на Javascript и содержит в себе “BrowserBot”. Selenium Remote Control – это дополнительный сервер, который потребуется для запуска тестов приведенных в этой статье. Он позволяет использовать любой язык программирования для взаимодействия с Selenium Core в браузере через посылку простых HTTP GET запросов на сервер RC. Все это звучит сложно, но работать с ним просто.

Обратите внимание, что прошло много времени с момента последнего релиза Selenium RC 0.9.0 (привет 2006 году),в этой статье я рекомендую использовать снапшот Selenium 0.9.2. Это необходимо, поскольку версия 0.9.0 не очень хорошо работает с последними версиями Firefox 2 и Internet Explorer 7 (прим. последние релизы Selenium RC и Core были сделаны в январе 2009 (буквально вчера), поэтому плясать с бубном не нужно, но нет гарантий, что тесты из этой статьи заработают). Из архива Selenium RC потребуется только один JAR файл с названием “selenium-server-standalone.jar”.

Базовая форма приёмочных тестов с применением PHPUnit и Selenium RC очень проста.

/** PHPUnit_Extensions_SeleniumTestCase */
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class GoogleIndexTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected function setUp()
{
/**
* ‘*firefox’ => Firefox 1 or 2
* ‘*iexplore’ => Internet Explorer (all)
* ‘*custom /path/to/browser/binary => Любые другие браузеры (включая Firefox в Linux)
* ‘*iehta’ => экспериментальный встроенный IE
* ‘*chrome’ => Экспериментальный профайл для Firefox (не браузер Chrome)
*/
$this->setBrowser(‘*firefox’);
$this->setBrowserUrl(‘http://www.google.ie/’); // сайт, который будем тестировать
}

public function testTitle()
{
$this->open(‘http://www.google.ie/’); // открыть дефолтную страницу
$this->assertTitleEquals(‘Google’); // проверить существует ли ожидаемый заголовок (title).
}
}

Метод SetUp ()используется для определения наших тестов, начнём тут с указания какой браузер использовать. Firefox – один из возможных вариантов. Если Selenium RC не содержит ссылка на ваш браузер вы можете использовать префикс “*custom” и путь до браузера. Функция setBrowserUrl () устанавливает основной URL для тестов. Обычно это главная (начальная) страница вашего приложения (например, index.php). Страница доступна для тестов, только когда она открыта (если это не очевидно). Поэтому тесты лучше начинать с открытия страницы с помощью функции open().

Selenium Core (Javascript BrowserBot) работает в окне браузера, что происходит достаточно просто понять после небольшой практики. Существует пять концепций, которые необходимо понять с самого начала:

  1. Действия (Actions, Selenium Actions)
  2. Accessors
  3. Assertions
  4. Локатор элементов (Element Locators)
  5. Шаблоны (Patterns)

Действия просто все ожидаемые действия, которые могут манипулировать состоянием веб интерфейса. Действия включают в себя нажатие на ссылку, нажатие клавиш, позиционирование мыши (например, onmouseover) и так далее. Все, что пользователь может сделать, Selenium Actions могут сделать тоже. Многим действия может быть указано ожидать определенного состояния. Самым простым является суффикс “AndWait”, который можно прикрепить к действиям, например, clickAndWait(). Это указывает Selenium, что после нужно отправить запрос на веб сервер и дождаться ответа перед тем как продолжить. Из-за изворотливого характера Selenium RC я рекомендую вам избегать использования “WaitFor” (ужасно работает в версии 0.9 и снапшоте 0.9.2 (прим. в 1.0 вроде ничего так)) и использовать отдельный вызов “waitForPageToLoad()”, – мы рассмотрим это действие позже.

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

Assertions так же, как и Accessors оценивают состояние. Но они также позволяют сравнить хранимое значение с ожидаемым. Есть два основных типа “assert (прим. упрощая – это жесткое сравнение – хоть тресни, а должно быть)” и “verify (проверить)”. В Selenium Core неравенство при assert приведет к окончанию теста, в то время как неравенство при сравнении приведет только к записи в лог.
Чтобы получить информативный результат теста, попробуйте использовать методы assert только при крайней необходимости, где неравенство или несоответствие делает продолжение теста бессмысленным.

NB: При тестировании AJAX веб-интерфейсов “WaitFor” методы чрезвычайно удобны для определения того, когда AJAX манипулирует с DOM, после чего вы можете использовать обычные verify / assert. Лучше всего в таких случаях функция waitForCondition(), которая использует Javascript выражения, делающие проверки, в цикле или в течение определенного времени.

Полный список возможных действий, Accessors и Assertions существующих в Selenium Core тут: http://www.openqa.org/selenium-core/reference.html. Список большой, так что при тестировании можно долго развлекаться.

Два последних понятия это локатор элементов и шаблоны. Локатор – это метода для поиска элементов HTML/XML для тестирования. Selenium Core поддерживает поиск с помощью идентификатора и имени элемента, выражений Javascript DOM, XPath, CSS селекторов, и текста ссылки.

Шаблоны это методы для определения текста для поиска. По умолчанию используется метод “glob” (например, «*»/«?»). Также можно использовать точный поиск (полное совпадение). Ну, и где бы мы были без “регулярного выражения”! Несколько примеров я приведу позже.

Реализация пользовательских историй (или – вот одна из подготовленных ранее)

Чтобы не уходить в сторону от темы, будем считать, что пользовательские истории уже реализованы в коде. Необходимо скачать и установить локальную копию. Код написан с использованием Zend Framework), и уже протестирован с PHPUnit и Slenium.
http://downloads.astrumfutura.org/devzone/Acceptance_Testing_Tutorial_Application.tar.bz2 (прим. файл доступен)

Для установки, просто импортируйте /INSTALL.sql в базу данных, скопируйте файл /src/config/config.ini.dist в /src/config.ini и поменяйте настройки под ваше окружение, тоже самое сделайте для /tests/TestConfiguration.php. В этом конфигурационном файле, вы можете изменить значение TESTS_SELENIUM_BROWSER на любой другой вариант (список выше в примере). Baseurl также должен быть изменен и указывать на наше приложение в /WWW каталоге.

Что еще? Предположим, что установка была в корень сайте: зайдите на http://localhost/www/. Вы можете запустить все тесты, которые мы напишем ниже, они расположены тут: http://localhost/tests/AllTests.php

Написание и проведение приёмочных испытаний

Приступим к делу. Я уверен, что достаточно теории на этот момент! Итак, какой был первый приёмочный тест поставленный клиентом?

1. Страница авторизации отображает форму для входа

Наша форма для авторизации находится по адресу “/login” главного файла приложения. Это тот самый URL который нужно открыть в Selenium Core. Во-вторых, наш тест должен проверять наличие регистрационной формы на странице. Мы уже (см. /src/default/views/scripts/login_index.phtml) обозначили форму для входа, и установили уникальный идентификатор “login-form”, по которому мы можем её найти. Также мы уже знаем (из того же шаблона), что должны существовать два поля с идентификаторами “identity” и “password”. Для расширения кругозора мы будем использовать разные шаблоны в локаторе элементов

/** PHPUnit_Extensions_SeleniumTestCase */
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';

class LoginIndexTest extends PHPUnit_Extensions_SeleniumTestCase
{
protected function setUp()
{
$this->setBrowser(‘*firefox’); // или *iexplore для IE
$this->setBrowserUrl(‘http://localhost/’);
}

public function testLoginFormExists()
{
$this->open(‘http://localhost/login’);

$this->assertElementPresent(“id=login-form”);
$this->assertElementPresent(“dom=document.forms[‘login-form’].identity”);
$this->assertElementPresent(“dom=document.forms[‘login-form’].password”);
$this->assertElementPresent(“xpath=//form[@id=’login-form’]/input[@type=’submit’]”);
}
}

Если вы скачали готовое приложение, то уже, вероятно запускали этот тест. Полный тест файл находится в файле /tests/AcceptanceTests/UserLoginTest.php. Возможно, вы уже догадываетесь, что добавление браузера и URL напрямую в код приведет к постоянным изменениям – поэтому в этих тестах используется набор констант из файла /tests/TestConfiguration.php.dist.

В примере выше встречается функция assertElementPresent(). Это проверка на наличие определенных элементов HTML / XML. Аргументом является строка, где тип локатора указывается слева от знака равно, а аргумент справа. Я использовал три различных типа. ID проверяет существование элемента с ID равным “login-form”. Наличие двух полей ввода проверяется с помощью выражений Javascript DOM. Наличие кнопки проверяется с помощью XPath.

Достаточно ли этих тестов? Существует риск, что если мы будем делать тесты слишком конкретизированными, то потом потребуется много времени на их редактирование при изменении интерфейса. Тем не менее, кое что еще должно быть проверено. Например, совпадает ли максимальная длина поля (maxlength) с длиной поля VARCHAR в базе данных или превышает его?

$this->assertElementPresent("xpath=//input[@id='identity' and @maxlength='20']");
$this->assertElementPresent("xpath=//input[@id='password' and @maxlength='64']");

2. Отправка правильного сочетания идентификатора пользователя и пароля приводит к успешной авторизации.

Теперь мы добавим тест, который будет отправлять валидные данные пользователя и проверять появилось ли сообщение, определенное в темплейте (см. темлейт login_index.phtml)

public function testValidAuthentication()
{
$this->open('http://localhost/login');

// заполняем форму
$this->type(“dom=document.forms[‘login-form’].identity”, ‘Padraic’);
$this->type(“dom=document.forms[‘login-form’].password”, ‘KSkjduj$!hjj*927′);

// отправляем данные
$this->click(“xpath=//form[@id=’login-form’]/input[@type=’submit’]”);
$this->waitForPageToLoad(30000); // 30 секунд по умолчанию

// проверяем сообщение
$this->assertTextPresent(‘exact:Welcome, Padraic’);

// проверяем, что нет сообщений об ошибке
$this->assertTextNotPresent(‘*invalid*’);
}

В примере виден ряд новых действий (action) и assertion методов. Первый type().type() имитирует ввод данные пользователем в текстовое поле, и мы используем его здесь, чтобы Selenium Core заполнил регистрационную форму. Действие click() определяет элемент для которого Selenium Core смоделирует нажатие (есть также метод submit() который можно использовать вместо click()). Дополнительный метод waitForPageToLoad() указывает Selenium, что после запроса будет ответ с сервера, и он должен ждать ответа перед тем, как продолжить. Оба PHPUnit и Selenium поддерживают метод clickAndWait(), который объединяет методы click() и waitForPageToLoad(). На момент написания статьи этот метод не работал, видимо будет в «следующем релизе», ну что же подождем и посмотрим…

Также в примере представлены методы assertTextPresent() и assertTextNotPresent(). Оба метода содержат шаблоны. Если вы вспомните, шаблоны используются для поиска текста на странице, используя различные методы, включая поиск glod, регулярные выражения и точное соответствие. В примере используется точное соответствие. Если вы не определите тип шаблона, по умолчанию будет использован “glob”. Хотя это и не плохо, вы, возможно, промахнетесь при поиске строк, которые содержат элементы glob – звездочка или знак вопроса.

3. Она гарантирует быстрое определение будущего поведения, которое отличается от ожидаемого (регрессионное тестирование)


public function testInvalidAuthenticationWithError()
{
$this->open('http://localhost/login');

// заполняем
$this->type(“dom=document.forms[‘login-form’].identity”, ‘Maugrim’);
$this->type(“dom=document.forms[‘login-form’].password”, ‘badpass’);

// отсылаем
$this->click(“xpath=//form[@id=’login-form’]/input[@type=’submit’]”);
$this->waitForPageToLoad(30000); // 30 secs

// проверяем, что логин не прошел
$this->assertTextNotPresent(‘regexp=Welcome,[a-zA-Z0-9 -_aouieAOUIE]’);

// проверяем, что ошибка появилась на странице
$this->assertTextPresent(‘*invalid*’);
}

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

4. Форма для логина всегда сопровождается гиперссылкой “Забыли логин или пароль?”.

Это больше изменение теста testLoginFormExists, чем новый тест. Мы просто добавим следующее:

public function testLoginFormExists()
{
//  ...

// проверяем существует ли ссылка
$this->assertElementPresent('link=regexp:^Forgot identity or password?$');
}

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

Как запускать тесты?

1. Запуск сервера Selenium RC

java -jar selenium-server-standalone.jar

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

2. Запустите файл AllTests.php из каталога /test в браузере.

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

Заключение

Приёмочное тестирование – это ценная обучающая практика. При оценке как приложение работает против набора тестовых ожиданий мы можем оценить завершенность продукта в целом и попадание в сроки разработки. При отсутствии приёмочного тестирования у нас нет уверенности, и придется тратить больше времени на ручное тестирование приложений. Ручное тестирования требует гораздо больше времени, и оно в меньшей степени, охватывает все потенциальные проблемы. Кроме того, интеграционные и юнит тесты не являются заменой приёмочным. Приёмочные тесты – это просто, и требуют совсем немного практики, чтобы начать работу. Я надеюсь, что эта статья поможет вам на этом поприще!

Leave a Reply

Your email address will not be published. Required fields are marked *