DevPoint: Selenium в тестировании веб-приложений
Что делать когда:
- Достался хард-кодный проект непокрытый тестами;
- код желает лучшего, а времени на рефакторинг нет;
- внесение правок в одном месте нарушает работу логики в другом;
- для покрытия *Unit тестами, проще переписать проект;
- бизнес логика размыта по коду и даже по шаблонам.
Забить и оправдываться, что такой код не должен жить?
С такими ситуациями очень часто сталкивался и меня это не устраивало. При поиске подходящего метода/инструмента тестирования я наткнулся на Selenium. И применяю его уже более 3-х лет.
В Киеве 9-го апреля прошла конференция DevPoint, посвященная web — разработке. Организатором данного мероприятия была компания Uniweb. В рамках ее, решил поделиться впечатлением про Selenium.
Selenium состоит из множества подпроектов, но выделить хотел только три:
Selenium Core — JavaScript Framework для написание и выполнения тестов. Используется в Selenium IDE и Remote Control*.
Selenium IDE — плагин для Firefox, который позволяет записывать и воспроизводить тесты. Также может генерировать код тестов для использования в Selenium Remote Control.
Selenium Remote Control — клиент-серверная система, которая позволяет Вам управлять веб-браузерами локально, или на других компьютерах, используя практически любой язык программирования
В рамках этого доклада про Selenium Core не было времени акцентировать внимание, хотя этот проект наиболее интересен для написания тестов с нетривиальной логикой.
Selenium IDE
- Интуитивно понятный интерфейс;
- возможность записи действия пользователя;
- с написанием тестов справится любой человек понимающий как должен работать проект;
- автоматическое генерирование *Unit кода для различных языков программирования.
Ложка дёгтя:
- Только для FireFox;
- после записи действия все равно нужно “допиливать ручками”;
- нет встроенного инструмента для получения XPath элементов.
Применяем Selenium IDE на практике
Для примера взял живой старый проект, который покрыть тестами задача еще та. Это обычный интернет-магазин, который используется для внутренних оптовых закупок в одной компании, имен не называем…
Первый тест, который мы напишем будет просто авторизоваться в системе:
Рекомендации для тех, кто начинает использовать Selenium:
- Не забывать команду waitForPageToLoad;
- применять максимально чаще команды assert* и verify*;
- после waitForPopUp не забывайте команду selectPopUp;
- после закрытия popUp – selectWindow;
- четко понимайте разницу между click и clickAndWait;
- при тестирование ajax частей применяйте команды waitFor*.
В системе очень важный функционал связанный с курсом валют, так как он должен задаваться вручную на каждый день. Напишем тест покрывающий эту логику:
И последних два теста, которые покрывают логику создания и редактирования заказа:
И для проверки наших тестов было намерено испорчено сохранение заказа:
Преимущества тестирования в IDE:
- Последовательное выполнения тестов в suite;
- удобное внесение изменений в тесты.
Минусы:
- Только для FireFox;
- инициализацию данных приходится делать вручную;
- невозможность протестировать cron скрипты.
Selenium Remote Control
Selenium Remote Control — это http демон, который принимает команды через GET и выполняет их. API по общению с Selenium RC есть почти под все языки программирования. В данном докладе речь идет только про API для PHP, которое предоставляется c PHPUnit
Как уже говорилось ранние в Selenium IDE есть приятная опция по генерированию кода для *Unit:
Таким образом вы можете просто копировать код и выполнять его в своих PHPUnit suite.
Также в PHPUnit — Selenium есть возможность запускать тесты написанные в Selenium IDE:
1 2 3 4 5 6 7 8 9 10 |
<span style="color: #000000; font-weight: bold;">class</span> SeleneseTests <span style="color: #000000; font-weight: bold;">extends</span> PHPUnit_Extensions_SeleniumTestCase <span style="color: #009900;">{</span> <span style="color: #000000; font-weight: bold;">public</span> static <span style="color: #000088;">$seleneseDirectory</span> <span style="color: #339933;">=</span> <span style="color: #0000ff;">'/devpoint/ide'</span><span style="color: #339933;">;</span> <span style="color: #000000; font-weight: bold;">protected</span> <span style="color: #000000; font-weight: bold;">function</span> setUp<span style="color: #009900;">(</span><span style="color: #009900;">)</span> <span style="color: #009900;">{</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">setBrowser</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"*firefox"</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">setBrowserUrl</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"test.devpoint.com.ua/"</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #009900;">}</span> <span style="color: #009900;">}</span> |
Без напильника не обойтись…
- PHPUnit_Extensions_SeleniumTestCase не умеет интерпретировать suite файлы с Selenium IDE;
- для выполнения теста PHPUnit запускает всегда новый браузер;
- PHPUnit 3.4.x неправильно отрабатывает логику команд wait.
Выполнение тестов в Selenium IDE и в PHPUnit отличаются своим подходом. Selenium IDE предполагает, что тесты выполняемые в suite могут быть зависимы друг от друга, в то время как в PHPUnit каждый тест не зависимым от предыдущего*. И на каждый тест PHPUnit шлет команды на переинициализацию браузера и соответственно наши тесты выполняются с ошибкой, так как они зависят от первого теста. Для решения этой проблемы была написана логика выполнения suite файла созданного в Selenium IDE.
Для решения проблемы с выполнением команд wait* нужно рассмотреть как они выполняются в PHPUnit:
1 2 3 4 5 6 7 |
<span style="color: #b1b100;">for</span> <span style="color: #009900;">(</span><span style="color: #000088;">$second</span> <span style="color: #339933;">=</span> <span style="color: #cc66cc;">0</span><span style="color: #339933;">;</span> <span style="color: #339933;">;</span> <span style="color: #000088;">$second</span><span style="color: #339933;">++</span><span style="color: #009900;">)</span> <span style="color: #009900;">{</span> <span style="color: #b1b100;">if</span> <span style="color: #009900;">(</span><span style="color: #000088;">$second</span> <span style="color: #339933;">>=</span> <span style="color: #cc66cc;">60</span><span style="color: #009900;">)</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">fail</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"timeout"</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> try <span style="color: #009900;">{</span> <span style="color: #b1b100;">if</span> <span style="color: #009900;">(</span><span style="color: #0000ff;">"Заказы"</span> <span style="color: #339933;">==</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #990000;">getText</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"//html/body/table/tbody/tr/td[2]/form/table/tbody/tr/td"</span><span style="color: #009900;">)</span><span style="color: #009900;">)</span> <span style="color: #b1b100;">break</span><span style="color: #339933;">;</span> <span style="color: #009900;">}</span> catch <span style="color: #009900;">(</span>Exception <span style="color: #000088;">$e</span><span style="color: #009900;">)</span> <span style="color: #009900;">{</span><span style="color: #009900;">}</span> <span style="color: #990000;">sleep</span><span style="color: #009900;">(</span><span style="color: #cc66cc;">1</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #009900;">}</span> |
Фактически мы зацикливаем выполнение команды, на определенный интервал и ждем пока наше условие не станет true. Реализация PHPUnit — Selenium посылая команду Selenium RC ждет от нее только два ответа, что все хорошо или что все плохо. Если пришел ответ ERROR, то он сразу закрывает браузер, пишет что произошла ошибка и соответственно наш цикл будет слать команды в уже закрытую сессию Selenium RC.
Код с решением этих проблем я выложил на github и не буду на нем останавливаться.
Еще из приятных вещей в Selenium RC то, что он умеет делать скриншоты при обнаружении ошибки:
1 2 3 4 5 6 |
<span style="color: #000000; font-weight: bold;">class</span> ScreenshotTest <span style="color: #000000; font-weight: bold;">extends</span> PHPUnit_Extensions_SeleniumTestCase <span style="color: #009900;">{</span> <span style="color: #000000; font-weight: bold;">protected</span> <span style="color: #000088;">$captureScreenshotOnFailure</span> <span style="color: #339933;">=</span> <span style="color: #009900; font-weight: bold;">true</span><span style="color: #339933;">;</span> <span style="color: #000000; font-weight: bold;">protected</span> <span style="color: #000088;">$screenshotPath</span> <span style="color: #339933;">=</span> <span style="color: #0000ff;">'/home/…/screenshots'</span><span style="color: #339933;">;</span> <span style="color: #000000; font-weight: bold;">protected</span> <span style="color: #000088;">$screenshotUrl</span> <span style="color: #339933;">=</span> <span style="color: #0000ff;">'http://localhost/screenshots'</span><span style="color: #339933;">;</span> <span style="color: #009900;">}</span> |
Минус в том, что на скриншотах не подсвечивается место возникновения ошибки, но по тексту в PHPUnit обычно легко понять что не так.
Дергаем за ниточки не FireFox
По заявлениям разработчиков Selenium RC поддерживает следующие браузеры:
- chrome
- iexplore
- firefox3
- googlechrome
- konqueror
- firefox2
- safari
- opera
На самом деле добиться корректного выполнения в этих браузерах не всегда можно с первого раза, но об этом не в рамках этого доклада. Единственное, что отмечу в Firefox, IE и Chrome под Windows проблем практически никогда не возникает и в Safari под MacOS.
Для запуска наших тестов в разных браузерах достаточно описать в статической переменной $browsers массив с настройками доступа к Selenium RC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<span style="color: #000000; font-weight: bold;">class</span> SeleneseTests <span style="color: #000000; font-weight: bold;">extends</span> PHPUnit_Extensions_SeleniumTestCase <span style="color: #009900;">{</span> <span style="color: #000000; font-weight: bold;">public</span> static <span style="color: #000088;">$browsers</span> <span style="color: #339933;">=</span> <span style="color: #990000;">array</span><span style="color: #009900;">(</span> <span style="color: #990000;">array</span><span style="color: #009900;">(</span> <span style="color: #0000ff;">'name'</span> <span style="color: #339933;">=></span> <span style="color: #0000ff;">'Firefox on Windows'</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'browser'</span> <span style="color: #339933;">=></span> <span style="color: #0000ff;">'*firefox'</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'host'</span> <span style="color: #339933;">=></span> <span style="color: #0000ff;">'localhost'</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'port'</span> <span style="color: #339933;">=></span> <span style="color: #cc66cc;">4444</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'timeout'</span> <span style="color: #339933;">=></span> <span style="color: #cc66cc;">30000</span><span style="color: #339933;">,</span> <span style="color: #009900;">)</span><span style="color: #339933;">,</span> <span style="color: #990000;">array</span><span style="color: #009900;">(</span> <span style="color: #0000ff;">'name'</span> <span style="color: #339933;">=></span> <span style="color: #0000ff;">'IE on Windows'</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'browser'</span> <span style="color: #339933;">=></span> <span style="color: #0000ff;">'*iexplore'</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'host'</span> <span style="color: #339933;">=></span> <span style="color: #0000ff;">'localhost'</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'port'</span> <span style="color: #339933;">=></span> <span style="color: #cc66cc;">4444</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'timeout'</span> <span style="color: #339933;">=></span> <span style="color: #cc66cc;">30000</span><span style="color: #339933;">,</span> <span style="color: #009900;">)</span><span style="color: #339933;">,</span> <span style="color: #990000;">array</span><span style="color: #009900;">(</span> <span style="color: #0000ff;">'name'</span> <span style="color: #339933;">=></span> <span style="color: #0000ff;">'Google Chrome on Windows'</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'browser'</span> <span style="color: #339933;">=></span> <span style="color: #0000ff;">'*googlechrome'</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'host'</span> <span style="color: #339933;">=></span> <span style="color: #0000ff;">'localhost'</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'port'</span> <span style="color: #339933;">=></span> <span style="color: #cc66cc;">4444</span><span style="color: #339933;">,</span> <span style="color: #0000ff;">'timeout'</span> <span style="color: #339933;">=></span> <span style="color: #cc66cc;">30000</span><span style="color: #339933;">,</span> <span style="color: #009900;">)</span><span style="color: #339933;">,</span> <span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #000000; font-weight: bold;">protected</span> <span style="color: #000000; font-weight: bold;">function</span> setUp<span style="color: #009900;">(</span><span style="color: #009900;">)</span> <span style="color: #009900;">{</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">setBrowserUrl</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"test.devpoint.com.ua/"</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #009900;">}</span> publicfunction testSigninCase<span style="color: #009900;">(</span><span style="color: #009900;">)</span> <span style="color: #009900;">{</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">open</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"/login/"</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">waitForPageToLoad</span><span style="color: #009900;">(</span><span style="color: #0000ff;">""</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">type</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"login"</span><span style="color: #339933;">,</span><span style="color: #0000ff;">"admin"</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">type</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"password"</span><span style="color: #339933;">,</span><span style="color: #0000ff;">"admin"</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">click</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"//input[@value='Войти']"</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">waitForPageToLoad</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"30000"</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #004000;">assertEquals</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"Главная"</span><span style="color: #339933;">,</span><span style="color: #000088;">$this</span><span style="color: #339933;">-></span><span style="color: #990000;">getText</span><span style="color: #009900;">(</span><span style="color: #0000ff;">"//html/body/table/tbody/tr/td/a"</span><span style="color: #009900;">)</span><span style="color: #009900;">)</span><span style="color: #339933;">;</span> <span style="color: #009900;">}</span> <span style="color: #009900;">}</span> |
C чем Selenium Вам не поможет?
- Тестирование загрузки файлов;
- тестирование cron скриптов;
- нет нормального решения для Flash части (http://code.google.com/p/flash-selenium).
Доклад
В качестве заключения мы рассмотрим несколько типичных команд. Это будут, пожалуй, самые востребованные при создании тестов команды.
open - открывает страницу по заданному URL.
click/clickAndWait – совершает клик, и, при необходимости, дожидается загрузки страницы.
verifyTitle/assertTitle - проверяет соответствие заголовка страницы ожидаемому.
verifyTextPresent - проверяет наличие ожидаемого текста где-либо на странице.
verifyElementPresent - проверяет страницу на наличие ожидаемого элемента по его HTML-тегу.
verifyText- проверяет наличие на странице ожидаемого текста и соответствующего ему HTML-тега.
verifyTable - проверяет таблицу на наличие ожидаемого содержимого.
waitForPageToLoad - временно прекращает выполнение теста до загрузки ожидаемой страницы. Автоматически вызывается при использовании команды “clickAndWait”.
waitForElementPresent - приостанавливает выполнение до появления ожидаемого элемента интерфейса пользователя с определенным HTML-тегом.
После waitForPopUp не забывайте команду selectPopUp;
После закрытия popUp – selectWindow;