Юнит-тесты уровня браузера на связке Selenium + PHP.

Обычно у проекта есть ряд важных тонких мест, которые просто обязаны быть покрыты юнит-тестированием.

Selenium предоставляет уникальную возможность проводить тестирование "от лица пользователя", на уровне операций браузера.

С помощью Selenium можно покрыть кросс-браузерными тестами сложный javascript-интерфейс.

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

Как устроен Selenium? (очень кратко)

Selenium - это java-программа, которая умеет запускать браузер и делать в нем различные действия типа клика на кнопку, поиска элемента, ожидания загрузки страницы.

Как устроен Selenium? (кратко)

Selenium - это HTTP-сервер, написанный на java (на основе Jetty).

Он принимает команды в простом текстовом формате. Причем, можно как набирать команды в "серверной консоли", так и посылать их, присоединившись к порту 4444.

Интеграция с языками программирования - это классы, которые предоставляют методы для удобной посылки команд серверу.

Например, вызов метода open("http://javascript.ru") посылает селениум-серверу на порт 4444 команду вида cmd=open&1=http://javascript.ru, а селениум-сервер, в свою очередь, отправит ее на исполнение в браузер.

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

При работе с сервером напрямую - сессию надо добавлять к каждой команде самостоятельно.

Запускаем селениум

Софт

Для запуска автоматического тестирования Selenium нам понадобятся:

  1. Java 1.5+
  2. PHPUnit и Testing_Selenium из PEAR:
    pear channel-discover pear.phpunit.de
    pear install channel://pear.phpunit.de/PHPUnit
    # на момент написания статьи версия 0.4.3 последняя бета
    pear install channel://pear.php.net/Testing_Selenium-0.4.3
    
  3. Selenium: качайте последнюю версию с http://selenium-rc.openqa.org/download.html

Старт сервера

В архиве selenium-remote-control содержатся API для разных языков программирования и сервер selenium-server.

Мы стартуем сервер в интерактивном (ключ -interactive) режиме, который позволяет запускать команды непосредственно из консоли.

# В каталоге с selenium-server запускаем
# java -jar selenium-server.jar -interactive
# предполагатся, что java - на пути PATH

C:\...\selenium-server-1.0-beta-1>java -jar selenium-server.jar -interactive
14:23:08.312 INFO - Java: Sun Microsystems Inc. 10.0-b22
14:23:08.312 INFO - OS: Windows XP 5.1 x86
14:23:08.312 INFO - v1.0-beta-1 [2201], with Core v1.0-beta-1 [1994]
14:23:08.390 INFO - Version Jetty/5.1.x
14:23:08.406 INFO - Started HttpContext[/selenium-server/driver,/selenium-server/driver]
14:23:08.406 INFO - Started HttpContext[/selenium-server,/selenium-server]
14:23:08.406 INFO - Started HttpContext[/,/]
14:23:08.406 INFO - Started SocketListener on 0.0.0.0:4444
14:23:08.406 INFO - Started org.mortbay.jetty.Server@201f9
Entering interactive mode... type Selenium commands here (e.g: cmd=open&1=http://www.yahoo.com)

Итак, селениум-сервер запустился и слушает порт 4444. Последняя строка демонстрирует пример команды.

Общий вид команд: cmd=(ИМЯ)&1=(Параметр1)&2=(Параметр2)...&sessionId=(СЕССИЯ)

Проверка, как что работает

Опция -interactive разрешает серверу принимать команды из консоли.
Поэтому можно тут же, из консоли, проверить, работает ли селениум - открыть http://www.google.com браузером Internet Explorer.

Открываем браузер

Для начала работы с селениум нужно открыть новую сессию. В сессии указывается тип браузера (*iexplore, *firefox, *opera и т.п.) и урл, с которого этот браузер начнет работу.

Будем тестировать в Internet Explorer, начнем работу с google.com.

Для этого введем команду getNewBrowserSession с аргументами *iexplore и http://www.google.com:

cmd=getNewBrowserSession&1=*iexplore&2=http://www.google.com

Откроется Internet Explorer с длинным URL вида http://www.google.com/selenium-server/core/...

cmd=getNewBrowserSession&1=*iexplore&2=http://www.google.com
14:23:22.921 INFO - ---> Requesting http://localhost:4444/selenium-server/driver?cmd=getNewBrowserSession&1=*iexplore&2=http://www.google.com
14:23:23.000 INFO - Checking Resource aliases
14:23:23.000 INFO - Command request: getNewBrowserSession[*iexplore, http://www.google.com] on session null
14:23:23.000 INFO - creating new remote session
14:23:23.343 INFO - Allocated session 42eb52b4dcfb453ab6938b4be8736b2b for http://www.google.com, launching...
14:23:23.343 INFO - Backing up registry settings...
14:23:24.250 INFO - Modifying registry settings...
14:23:24.640 INFO - Launching Internet Explorer...
14:23:27.421 INFO - Got result: OK,42eb52b4dcfb453ab6938b4be8736b2b on session 42eb52b4dcfb453ab6938b4be8736b2b

Вывод селениума сообщил, что создана сессия "Allocated session 42eb52b4dcfb453ab6938b4be8736b2b", и команда открытия успешно выполнена: "Got result: OK"

Все дальнейшие операции в этой сессии должны происходить в рамках исходного домена http://www.google.com.

Есть способы обойти это ограничение, запустив Selenium в привилегированном режиме: *iehta вместо *iexplore, или воспользовавшись другим способом, описанным в http://selenium-rc.openqa.org/experimental.html.

Однако, достаточно стабильную и безглючную работу Selenium мне удалось получить только в рамках одного домена.

Механизм работы Selenium

Selenium работает исключительно на уровне javascript, без привязки к API, DLL и прочим внутренностям браузера.

Запуская браузер командой cmd=getNewBrowserSession&1=*iexplore&2=http://www.google.com, Selenium ставит себя (localhost:4444) в настройках прокси. Собственно, эта настройка - и есть всё отличие в поведении браузера, запущенного через Селениум.

Селениум-сервер, работая как прокси, перехватывает все URL, которые начинаются с /selenium-server/ (в рамках исходного домена) и отдает свои страницы.

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

На страничке, которая открылась в браузере, есть длинный идентификатор: 42eb52b4dcfb453ab6938b4be8736b2b - это сессия. Все дальнейшие команды, которые вы отправите селениум-серверу с этой сессией, будут выполнены в этом браузере. При этом неважно откуда они пришли: по порту 4444 или вручную из консоли.

Переходим на URL

Для перехода на URL служит команда open. Не забываем указать сессию:

cmd=open&1=http://www.google.com&sessionId=42eb52b4dcfb453ab6938b4be8736b2b

Google открылся. С виду все хорошо. Но глянем на консоль:

cmd=open&1=http://www.google.com&sessionId=42eb52b4dcfb453ab6938b4be8736b2b
14:37:32.843 INFO - ---> Requesting http://localhost:4444/selenium-server/driver?cmd=open&1=http://www.google.com&sessionId=42eb52b4dcfb453ab6938b4be8736b2b
14:37:32.859 INFO - Command request: open[http://www.google.com, ] on session 42eb52b4dcfb453ab6938b4be8736b2b
14:37:38.078 INFO - Got result: Разрешение отклонено on session 42eb52b4dcfb453ab6938b4be8736b2b

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

Когда-то я потратил небольшое энное количество времени в поисках - что не так и почему оно не пашет.

Разгадка оказалось простой. Google самостоятельно перенаправил браузер с http://www.google.com на http://www.google.ru. А сессия была запущена на google.com. Поэтому, следуя политике безопасности Same Origin, браузер показал селениуму фигу.

Чтобы такого не было, следует с самого начала выбрать нужный домен правильно. В нашем случае правильный выбор - www.google.ru. И в дальнейшем избегать кросс-доменных редиректов.

Для тестирования поисковика Google мы используем новые команды Selenium. Их список и описание которых можно найти в документации.

Алгоритм теста поисковика Google:

  1. запустить браузер
    cmd=getNewBrowserSession&1=*iexplore&2=http://www.google.ru
    

    Selenium выдаст сессию. Для краткости, обозначим ее 12345.

  2. открыть страницу
    cmd=open&1=http://www.google.ru/&sessionId=12345
    
  3. заполнить поле с именем q строкой поиска:
    cmd=type&1=q&2=selenium&sessionId=12345
    
  4. кликнуть на кнопку "поиск" (ее id=btnG)
    cmd=click&1=btnG&sessionId=12345
    
  5. проверить при помощи XPath, есть ли (isElementPresent) ссылки со словом Selenium
    cmd=isElementPresent&1=//a[contains(text(),"Selenium")]
    ...
    Got result: OK,true on session 12345
    

    да, такие ссылки есть

  6. завершить тестирование
    cmd=testComplete&sessionId=12345
    

    При таком завершении селениум сам закроет браузер и аккуратно удалит все временные файлы.

  7. Пишем первый автоматизированный тест

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

    Пример Google

    Пример такого теста есть в архиве селениума в каталоге selenium-php-client-driver. Например, GoogleTest.php. Но версия из архива на русском google работать не будет, поэтому вот модифицированный вариант:

    <?php
    // GoogleTest.php
    // должны быть установлены PEAR-пакеты
    // сам PEAR должен быть в include_path
    require_once 'Testing/Selenium.php';
    require_once 'PHPUnit/Framework/TestCase.php';
    
    class GoogleTest extends PHPUnit_Framework_TestCase
    {
        private $selenium;
    
        public function setUp()
        {
            $this->selenium = new Testing_Selenium("*iexplore", "http://www.google.ru");
            $this->selenium->start();
        }
    
        public function tearDown()
        {
            $this->selenium->stop();
        }
    
        public function testGoogle()
        {
            $this->selenium->open("/");
            $this->selenium->type("q", "hello world");
            $this->selenium->click("btnG");
            $this->selenium->waitForPageToLoad(10000);
            // русский текст в кодировке UTF-8 !
            $this->assertRegExp("/Поиск в Google/", $this->selenium->getTitle());
        }
    }
    

    Итак, проверив что Selenium-сервер работает, запускаем тест из директории с файлом GoogleTest.php :

    C:\...>phpunit GoogleTest.php
    PHPUnit 3.2.21 by Sebastian Bergmann.
    
    .
    
    Time: 7 seconds
    
    OK (1 test)
    
    C:\...>
    

    В классе была всего одна функция, имя которой начинается на test.., поэтому тест один.

    Если что-то не работает, то подробный лог будет в консоли selenium-сервера.

    Схема тестирования

    Авторизация - один из самых критичных сервисов сайта. Будем тестировать авторизацию на сервере http://mail.ru.

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

    Схема теста по шагам:

    1. Зайти на заглавную
    2. Заполнить логин-пароль и кликнуть на Войти
    3. Проверить, что появилась кнопка Выход
    4. Кликнуть на выход, проверить что появилась кнопка Войти

    Заметим, что mail.ru редиректит на домен win.mail.ru. Чтобы тестирование работало - нужно сразу зайти на win.mail.ru, аналогично тесту для Google.

    Тест авторизации на mail.ru

    Код файла MailTest.php:

    <?php
    // MailTest.php
    
    require_once 'Testing/Selenium.php';
    require_once 'PHPUnit/Framework/TestCase.php';
    
    class MailTest extends PHPUnit_Framework_TestCase
    {
        protected $selenium;
    
        // XPATH-локатор для кнопки "Войти"
        protected $enterLocator = "//input[@type='submit' and @value='Войти']";
        
        // XPATH-локатор для кнопки "Выйти"
        protected $exitLocator = "//input[@type='submit' and @value=' Выход ']";
        
        /*
         * инициализация теста
        */
        public function setUp()
        {
            // Если браузера нет на пути PATH, нужно указать полный путь
            $opera = "*opera C:\Program Files\Opera 9\opera.exe";        
            
            $ie = "*iexplore";
            
            // в процессе авторизации сервер mail.ru перенаправляет на домен win.mail.ru
            // чтобы тест работал корректно, нужно сразу зайти на win.mail.ru.
            $this->selenium = new Testing_Selenium($ie, "http://win.mail.ru");
            $this->selenium->start();
            
            // таймаут по умолчанию 30 секунд.
            // поставим 600 сек, т.к команда open ждет, пока браузер загрузит картинки
            $this->selenium->setTimeout(600000);
           
        }
    
    
        /*
         * тест авторизации 
        */
        public function testMail() {
            
            $this->selenium->open("/");
            
            // команда open выполняется синхронно, ожидая полной загрузки страницы
            
            // если браузер уже залогинен (например, режим "запомнить меня")
            if ($this->selenium->isElementPresent($this->exitLocator)) {
                // выйти
                $this->logout();
            }
            
            $this->login();
            $this->logout();        
        }
        
        /*
        * Выйти из сайта
        */
        public function logout() {
            
    
            // нажать на кнопку "выход"
            $this->selenium->click($this->exitLocator);
            
            // команда click, как и почти все команды, выполняется асинхронно.
            
            // надо подождать загрузки страницы, ждем 600 сек максимум
            $this->selenium->waitForPageToLoad(600000);
    
            // проверить, что появилась кнопка "войти"
            $this->assertTrue($this->selenium->isElementPresent($this->enterLocator));        
        }
        
        /*
        * Войти в сайт
        */
        public function login()
        {
            $this->selenium->type("Login", 'selenium_test');
            $this->selenium->type("Password", '123456');
            $this->selenium->click($this->enterLocator);        
            $this->selenium->waitForPageToLoad(10000);
            
            // проверить, что появилась кнопка "выйти"
            $this->assertTrue($this->selenium->isElementPresent($this->exitLocator));
            
        }
        
        /*
         * Завершение теста
        */
        public function tearDown()
        {
            $this->selenium->stop();
        }
    
    }
    

    Если что-то по тесту вдруг неочевидно - задайте Ваш вопрос в комментариях, я дополню описание.

    Фишки Selenium

    При практической работе с Selenium Вы столкнетесь с большим количеством фич и багов. Не пугайтесь. Вы не один такой. Вот некоторые из них.

    Firefox 3

    Для работы с Firefox 3 на момент написания статьи придется скачать последний снапшот Selenium RC, т.к версия 1.0-beta1 его запускать не умеет.

    Впрочем, с последним снапшотом хватает других глюков.

    Альтернативный вариант - запускать браузер с нужным профилем и прокси, используя тип *custom.

    Кроме того, некорректно завершенные (например, по ctrl-c) сессии Firefox оставляют во временной директории профили вида custom*. Их можно убивать. Иногда селениум ругается, что там какой-то лок-файл и запустить Firefox нельзя. Тогда все эти профили надо обязательно убить.

    Работа с confirm

    По умолчанию Selenium не показывает окошки подтверждения confirm и автоматом жмет на них OK.

    Есть методы, которые меняют это поведение.

    В любом случае, нужно обязательно вызвать метод getConfirmation сразу после появления подтверждения.
    Иначе последующие команды selenium'а не будут выполнены браузером.

    Загрузка файла

    Чтобы протестировать загрузку файла - нужно обойти ограничение безопасности в Javascript. По умолчанию javascript не может менять значение <input type="file">.

    В Firefox можно дать Selenium привилегии на загрузку файла, добавив вызов:

    netscape.security.PrivilegeManager.enablePrivilege("UniversalFileRead")
    

    в файл selenium-api.js в начало функции Selenium.prototype.doType.

    Кроме того, чтобы запрос привилегии сработал в "неподписанном" скрипте - нужно поставить в Firefox настройку "signed.applets.codebase_principal_support" в значение "true", например, найдя ее на страничке about:config.

    И тогда загрузки будут работать.

    Альтернативный выход - запустить браузер в экспериментальном привилегированном режиме (chrome/iehta/...) или через Proxy Injector. Но тогда готовьтесь к дополнительному набору глюков.

    Также по теме: Testing File Uploads with Selenium RC and Firefox.

    Как изучать Selenium?

    В документации по селениум - изрядный бардак. Возможно, к выходу 1.0 это поправят.

    1. Основной сайт Selenium RC: http://selenium-rc.openqa.org/.
      Обратите внимание на секцию Tutorial.
    2. В вики, куда постепенно мигрирует документация, находится FAQ.
    3. Дока по командам selenium и по локаторам элементов: Selenium Core Reference
    4. Много полезных расширений и дополнительных команд для селениум. Must Read: Contributed User-Extensions

    Рецептами решения глюков щедро поделится google и сайт поддержки OpenQA.

    Заключение

    Selenium - одна из немногих платформ, которые позволяют сделать интеграционные юнит тесты не на уровне кода или базы, а полностью - работает ли сайт.

    • Пожалуй, единственное средство для удобной автоматизированной эмуляции действий посетителя
    • Кросс-браузерное
    • Есть способы интеграции с множеством языков и систем тестирования. Для PHP - это PHPUnit.
    • Довольно глючная вещь. Заранее готовьтесь к борьбе с непонятками.
    • Почти не умеет работать с поддоменами. Редирект посетителя на другой домен обычно ломает тест.

    P.S В этой статье нет ни слова о Selenium IDE. Это не потому что оно того не заслуживает. Наоборот - Selenium IDE требует отдельной хорошей статьи.

    Успешного автоматизированного тестирования!