четверг, 27 ноября 2008 г.

Selenium и symfony

Selenium

В symfony есть вполне удобные инструменты для создания тестов, как юнит тестов, так и функциональных тестов. Но в некоторых случаях инструментов symfony для функционального тестирования не достаточно. Например когда тестами надо покрыть функциональность частично реализованную на javascript. Или для тестирования определенной функциональности под разными браузерами. Для этих целей можно использовать Selenium.

Selenium - это набор инструментов для автоматизированного тестирования веб приложений под разными платформами. Со всеми инструментами тестирования можно ознакомиться на официальном сайте. Здесь я хочу рассмотреть Selenium Remote Control (RC) - инструмент для написания тестов для Selenium с использованием PHP Client Driver. Проще говоря - я хочу рассказать о написании тестов под Selenium используя PHP при разработке на базе symfony.

Интеграция Selenium и Symfony

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

Итак, скачаем Selenium-Rc (на текущий момент лучше скачивать "latest nightly build", потому как у доступной сейчас версии "1.0 beta 1" есть некоторые проблемы с firefox 3). Собственно из всего архива нам потребуются selenium-server*/selenium-server.jar и selenium-php-client-driver*/PEAR/Testing. По мимо этого нам так же потребуется PHPUnit. Соответственно Testing и PHPUnit необходимо разместить так, что бы они были доступны при обращениях типа


require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
Можно их разместить в директории lib/vendor и прописать данный путь в include_path.

Создадим в нашем проекте (предпологается, что у нас создан проект и приложение frontend) плагин, скажем spSeleniumTestPlugin со следующей структурой:


plugins
  spSeleniumTestPlugin
    config
      settings.yml
    lib
      task
Теперь, создадим команду позволяющую запускать "selenium" тесты примерно следующей командой:

$ ./symfony selenium:test frontend
Для этого выполним следующую команду

$ ./symfony generate:task selenium:test --dir=plugins/spSeleniumTestPlugin/lib/task
Данная команда создаст класс-каркас для нашей команды в директории plugins/spSeleniumTestPlugin/lib/task. Так же разместим selenium-server.jar в дироектории plugins/spSeleniumTestPlugin/lib. И возьмемся за написание команды запускающей "selenium" тесты. Я не будет подробно останавливаться на создании команд для symfony, а просто выложу исходный код и немного его прокомментирую

require_once 'PHPUnit/Framework/TestSuite.php';
require_once 'PHPUnit/TextUI/TestRunner.php';

class seleniumTestTask extends sfBaseTask
{
    protected function configure()
    {
        $this->namespace        = 'selenium';
        $this->name             = 'test';
        $this->briefDescription = '';
        $this->detailedDescription = <<<EOF
The [selenium:test|INFO] task does things.
Call it with:

  [php symfony selenium:test|INFO]
EOF;
        $this->addArgument('application', sfCommandArgument::REQUIRED, 'The application name');
        $this->addArgument('directory', sfCommandArgument::OPTIONAL, 'Directory');
        $this->addOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'The environment', 'test');
    }

    protected function execute($arguments = array(), $options = array())
    {
        $dbManager = new sfDatabaseManager($this->configuration);
        $connection = Doctrine_Manager::connection();

        list($resource, $pipes) = $this->runSelenium(sfConfig::get('sf_selenium_port', 4444));

        $ds = DIRECTORY_SEPARATOR;
        $app = $arguments['application'];

        $testAppDir = sfConfig::get('sf_test_dir') . $ds . 'selenium' . $ds . $app;
        if ($arguments['directory'])
        {
            $testAppDir .=  $ds . $arguments['directory'];
        }

        $this->logSection('find tests in', $testAppDir);

        $files = sfFinder::type('file')
            ->ignore_version_control()
            ->follow_link()
            ->name('*Test.php')
            ->relative()
            ->in($testAppDir)
            ;

        $suite = new PHPUnit_Framework_TestSuite();

        foreach($files as $file)
        {
            $suite->addTestFile($testAppDir . $ds. $file);
        }

        $runner = new PHPUnit_TextUI_TestRunner;
        $runner->doRun($suite, array());

        proc_terminate($resource);
    }

    protected function runSelenium($port)
    {
        $ds = DIRECTORY_SEPARATOR;
        $pluginPath = realpath(dirname(__FILE__) . $ds . '..' . $ds . '..');

        $files = sfFinder::type('file')->ignore_version_control()->follow_link()->name('*selenium*server*.jar')->in($pluginPath);

        $seleniumJar = $files[0];
        $javaBin = sfConfig::get('sf_selenium_java_bin', 'java');

        $args = ' -port ' . $port;

        $cmd = escapeshellcmd($javaBin . ' -jar ' . $seleniumJar . $args);

        $dSpec = array
        (
            0 => array('pipe', 'r'),
            1 => array('pipe', 'w'),
            2 => array('pipe', 'r')
        );

        $pipes = array();
        $this->logSection('selenium', 'run');
        $resource = proc_open($cmd, $dSpec, $pipes);

        sleep(1);

        return array($resource, $pipes);
    }
}
В команде мы определяем два аргумента application и directory. Первый отвечает за то какое приложение будем тестировать, а второй не обязательный определяет в какой относительной директории искать тесты, это для того, что бы не запускать каждый раз все тесты, так как они могут выполняться достаточно долго.

Тесты по умолчанию ищутся в директории test/selenium/app_name - это для того, чтобы не смешивать их с остальными тестами (Примечание: по-хорошему надо бы реализовать интеграцию чтобы тесты могли располагаться вместе с остальными и запускаться общей для всех командой и этого добиться в общем не сложно, было бы время и желание). Так же в нашей команде определена одна не обязательная опция --env служащая для задания окружения.

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

Пример теста

Для примера создадим файл test/selenium/GoogleTest.php со следующим содержанием:


setBrowser('*firefox');
        $this->setBrowserUrl('http://google.com/');
    }

    public function testGoogle()
    {
        $this->open("http://google.com/");
        $this->type("q", "hello world");
        $this->click("btnG");
        $this->waitForPageToLoad(10000);
        $this->assertRegExp("/helloworld\.ru/i", $this->getBodyText());

    }
}
Данный тест выполняет следующее, запускает браузер firefox, переходит на google.com, вводит в строку поиска (поле "q") "hello world", кликает на кнопку "btnG", ждет ответа и проверяет, что в тексте body содержится строка "helloworld.ru". А теперь запустим его

$ ./symfony selenium:test frontend
Если на выходе у вас, что-то вроде:

>> selenium  run
>> find tests in /var/workspace/sf11/example/test/selenium/backend
PHPUnit 3.2.21 by Sebastian Bergmann.

.

Time: 7 seconds


OK (1 tests)
значит все прошло успешно. Из выводимых сообщений можно понять, что тесты выполнялись 7 секунд, был выполнен 1 тест, который успешно прошел. Если у вас что-то не сработало, пишите.

На этом пока все, в следующей заметке я постараюсь описать основные возможности selenium и API PHPUnit.

Комментариев нет: