Selenium and symfony Integeration – the Non-plugin Approach (Tutorial)
I have been writing a lot test cases for a large web project built by symfony. From time to time I found the build-in testers are not sufficient to test my application thorough enough. Often times it is not possible to simulate the way a user would interact with the application. For this reason, I have turned to Selenium.
Though it takes some extra work to get it to work with symfony's Lime test framework, the result turns out to be very satisfactory. The following is for those who are interested at the integration process. All code examples shown here are written using a Linux box, but they should also work under Window environment.
Running the Server - Selenium 2 RC
The tool we need from SeleniumHQ is "Selenium 2". Even though it is says "2.0 alpha 5" on their download page as time of writing, it is actually stable enough for our purpose. You may install "Selenium IDE" to your Firefox, but I don't find it is very useful. I only used it once a while. All downloads can be found on Selenium's site.After you have downloaded the server package, unzip it and start the server by running:
java -jar selenium-server-standalone-2.0a5-2.jar
You should see some output from your command prompt similar below:
01:43:37.950 INFO - Java: Sun Microsystems Inc. 16.3-b01 01:43:37.951 INFO - OS: Linux 2.6.31-22-generic amd64 01:43:37.960 INFO - v2.0 [a5], with Core v2.0 [a5] 01:43:38.065 INFO - RemoteWebDriver instances should connect to: http://192.168.1.111:4444/wd/hub 01:43:38.067 INFO - Version Jetty/5.1.x 01:43:38.068 INFO - Started HttpContext[/selenium-server/driver,/selenium-server/driver] 01:43:38.069 INFO - Started HttpContext[/selenium-server,/selenium-server] 01:43:38.069 INFO - Started HttpContext[/,/]
Getting Client Library for PHP
Go to the download site again, download Selenium RC (selenium-remote-control-1.x.x.zip). Note that we only need the client library, not the server package since we are using Selenium 2 server.
Unzip the file and navigate to selenium-php-client-driver-1.x.x/PEAR/Testing/, copy the all files under it to SF_ROOT_DIR/lib/test/selenium/. For my personal preference and to comply with symfony's convention, I have renamed the class files:
Selenium.php => Testing_Selenium.class.php
Exception.php => Testing_Selenium_Exception.class.php
I have placed two file to the same directory level:
SF_ROOT_DIR/lib/test/selenium/Testing_Selenium.class.php
SF_ROOT_DIR/lib/test/selenium/Testing_Selenium_Exception.class.php
Open SF_ROOT_DIR/lib/test/selenium/Testing_Selenium.class.php, and remove the line require_once 'Testing/Selenium/Exception.php';, since all class files will be auto loaded by symfony.
There's no whatsoever reason to use PEAR_Exception, so I have changed the content of Testing_Selenium.class.php to as the following:
<?php
/* comments omitted */
class Testing_Selenium_Exception extends Exception{
}
Extends Client Library
You may use the client library you just downloaded from SelemniuHQ, but I would like to separate my own code from the library code, so choose to extend the client library. I overwrote the doCommand method, to capture the output returned by Selenium server, also to call the callback function which outputs some useful message via symfony's Lime output.
Additional Selenium related methods should be placed into this class (or Testing_Selenium if you don't do the inheritance way). For example, I have added clickAndWait method, which will be used extensively in our testing code.
//SF_ROOT_DIR/lib/teset/mySelenium.class.php
<?php
class mySelenium extends Testing_Selenium {
protected $response;
protected $doCommandCallbackObject = null;
protected $doCommandCallbackMethod = null;
protected function doCommand($verb, $args = array()) {
$this->response = null;
$url = sprintf('http://%s:%s/selenium-server/driver/?cmd=%s', $this->host, $this->port, urlencode($verb));
for ($i = 0; $i < count($args); $i++) {
$argNum = strval($i + 1);
$url .= sprintf('&%s=%s', $argNum, urlencode(trim($args[$i])));
}
if (isset($this->sessionId)) {
$url .= sprintf('&%s=%s', 'sessionId', $this->sessionId);
}
if (!$handle = fopen($url, 'r')) {
throw new Testing_Selenium_Exception(
'Cannot connected to Selenium RC Server'
);
}
stream_set_blocking($handle, false);
$response = stream_get_contents($handle);
fclose($handle);
if ($this->doCommandCallbackObject != null) {
call_user_func_array(array(
$this->doCommandCallbackObject,
$this->doCommandCallbackMethod
), array($response, $verb, $args));
}
$this->response = $response;
return $response;
}
/**
* A short hand method for type(), accept array as parameter
* @param Array $fieldList
*/
public function typeMany($fieldList) {
foreach ($fieldList as $k => $v) {
$this->type($k, $v);
}
}
public function clickAndWait($locator, $timeout = 8000) {
$this->click($locator);
$this->waitForPageToLoad($timeout);
}
public function setDoCommandHook(&$object, $method) {
if (!is_object($object)) {
throw new Exception('Callback object must be an object instance');
}
$this->doCommandCallbackObject = $object;
$this->doCommandCallbackMethod = $method;
}
public function getResponse() {
return $this->response;
}
Extends sfBrowser
We extends sfBrowser to handle initialization and destruction of the Selenium test session. Here's the example code how to do it:
Added at: 2010/11/25
<?php
class myBrowser extends sfBrowser {
}
//SF_ROOT_DIR/lib/test/myBrowserSelenium.class.php
<?php
class myBrowserSelenium extends myBrowser {
protected $selenium;
protected $sessionId = null;
public function __construct($hostname = null, $remote = null,
$options = array(), $seleniumOptions = array()) {
if (strpos($hostname, 'http://') === false) {
$seleniumHostName = 'http://' . $hostname;
}
$seleniumOptions = array_merge(array(
'browserType' => '*firefox',
'hostname' => $seleniumHostName,
'captureNetworkTraffic' => true
), $seleniumOptions);
$this->initSelenium($seleniumOptions);
parent::__construct($hostname, $remote, $options);
}
protected function initSelenium($seleniumOptions = array()) {
$this->selenium = new mySelenium(
$seleniumOptions['browserType'],
$seleniumOptions['hostname']
);
$this->selenium->startWithOptions(array(
'captureNetworkTraffic' => $seleniumOptions
));
}
public function start() {
$this->sessionId = $this->selenium->start();
return $this;
}
public function stop() {
$this->sessionId = null;
$this->selenium->stop();
return $this;
}
public function getSessionId() {
return $this->sessionId;
}
public function __destruct() {
$this->stop();
}
public function getSelenium() {
return $this->selenium;
}
}
Extends sfTestFunctional
I extended sfTestFuncional to make a proxy method which returns mySelenium instance, and to support better code auto-completion.
//SF_ROOT_DIR/lib/test/myTestFunctionalSelenium.class.php
<?php
class myTestFunctionalSelenium extends sfTestFunctional {
/**
* @return mySelenium
*/
public function getSelenium() {
$browser = $this->getBrowser();
if (!$browser instanceof myBrowserSelenium) {
throw new Exception("Your browser instance is not myBrowserSelenium");
}
return $this->getBrowser()->getSelenium();
}
}
Create a Selenium Tester
I have created a tester which provides a number of methods that perform the testing job and output meaningful feedback information when the test is run.
Note that I overwrote the __call method, so when you call a method which does not exist in the tester class but in the mySelenium class, the method in mySelenium class will get invoked.
Read more about writing a new symfony tester
//SF_ROOT_DIR/lib/test/mySeleniumTester.class.php
<?php
class mySeleniumTester extends sfTester {
/** @var mySelenium */
protected $selenium;
public function __construct(myTestFunctionalSelenium $browser, $tester) {
parent::__construct($browser, $tester);
$this->selenium = $this->browser->getSelenium();
$this->selenium->setDoCommandHook($this, "doCommandFailed");
}
/**
* Prepares the tester.
*/
public function prepare() {
}
/**
* Initializes the tester.
*/
public function initialize() {
}
public function __call($method, $arguments) {
if (method_exists($this->selenium, $method)) {
call_user_func_array(array($this->selenium, $method), $arguments);
return $this->getObjectToReturn();
} else {
return parent::__call($method, $arguments);
}
}
/**
* Used internally, do not call this method directly
*/
public function doCommandFailed($response, $command, $args) {
if ($command != 'waitForPageToLoad' && strpos($response, 'OK') !== 0) {
$out = $command . ': ' . $response . "\n";
if (count($args) > 0) {
foreach ($args as $arg) {
$out .= ' ' . $arg . "\n";
}
}
$this->tester->fail($out);
}
}
Create a Factory Class to construct Functional Test Class Instance
You can skip this step, but you are going to use this code in almost all of your test cases. So I would suggest you use the following code as I did:
//SF_ROOT_DIR/lib/test/myBrowserFactory.class.php
<?php
class myBrowserFactory {
public static function createBrowser() {
$b = new myBrowser(sfConfig::get('app_some_url'));
return $b;
}
public static function createBrowserSelenium() {
$b = new myBrowserSelenium(
sfConfig::get('app_some_url''),
null,
array(),
array(
'browserType' => '*chrome'
)
);
return $b;
}
public static function createFunctionalTestBrowser() {
$b = new myTestFunctional(self::createBrowser());
return $b;
}
public static function createFunctionalTestBrowserSelenium() {
$b = new myTestFunctionalSelenium(self::createBrowserSelenium());
$b->setTester('selenium', 'mySeleniumTester');
return $b;
}
}
Almost there, please bear with me.
Create a Test Case
Create a new file under SF_ROOT_DIR/test/functional/[application_name]/homeTest.php, replace the "[application_name]" with the symfony application you wan to test. Create a test case a the following:
//SF_ROOT_DIR/test/functional/frontend/homeTest.php
<?php
include(dirname(__FILE__) . '/../../bootstrap/functional.php');
//create an instance of functional test browser
$b = myBrowserFactory::createFunctionalTestBrowserSelenium();
$b->with('selenium')->begin()
->info('Open url')
->open('http://thecodecentral.com')
->info('Click a link')
->clickAndWait("link=Home")
->isTextPresent('Some text to test')
//->isTextPresent('glob:wildcard test*')
//->isElementPresent('some selector, check documentation')
//->isElementPresent('//input[@id="myButton"]')
->typeMany(array(
'name' => 'something',
'url' => 'something',
))
//can you call sleep(second), which will pause the execution
//of the script by x seconds, very useful for testing Ajax request
//->sleep(10)
To run the test case, execute the following (assume that application name is "frontend"):
php symfony test:functional frontend home
You should see Firefox browser showing up and responding to your test commands.
Read more about Selenium commands
Selenium Test Case Writing Consideration
The key to writing Selenium test cases in symfony is not to pay too much attention to the underlaying implementation of the web application. Let your test cases test the web application as if a human user would. By doing so will ease your test creation because you will not have follow tightly what you read on the official documentation of symfony. I use almost only the $b->with('selenium')->begin() tester from now on.
When you are writing a test case, open a page you want to test, and imagine what the user would do, and then translate all the user actions to Selenium commands. For example,
| User | Selenium Test Code in symfony |
|---|---|
| Open a URL | ->open() |
| Click a link | ->clickAndWait() |
| Type some thing into text filed/area | ->type() / ->typeMany() |
| Check a checkbox / radio button | ->check() |
| Upload a file | ->attachFile() |
And so on...
You can use isElementPresent() and isTextPresent() to verify the resulting page.
Functional Bootstrap
The bootstrap file should be generated by symfony at SF_ROOT_DIR/test/bootstrap/functional.php by default. I post it here in case you don't have this file for some reason.
//SF_ROOT_DIR/test/bootstrap/functional.php
<?php
/*
* This file is part of the symfony package.
* (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
// guess current application
if (!isset($app))
{
$traces = debug_backtrace();
$caller = $traces[0];
$dirPieces = explode(DIRECTORY_SEPARATOR, dirname($caller['file']));
$app = array_pop($dirPieces);
}
require_once dirname(__FILE__).'/../../config/ProjectConfiguration.class.php';
$configuration = ProjectConfiguration::getApplicationConfiguration($app, 'test', isset($debug) ? $debug : true);
sfContext::createInstance($configuration);
$databaseManager = new sfDatabaseManager($configuration);
$databaseManager->loadConfiguration();
// remove all cache
sfToolkit::clearDirectory(sfConfig::get('sf_app_cache_dir'));
Source Download
To be added.
More interesting posts ...
Leave a Comment
If you would like to make a comment, please fill out the form below.
To my beloved readers:
Please note that you may freely post comments here, but I will most likely not be able to reply to most them due to my current availability.

Could you also post the code for the myBrowser class.
I guess you extend sfBrowser in a certain way.
Tanks man!
Thanks,
But i am getting an error.
When i try to run my test it says that the startWithOptions method in myBrowserSelenium class does not exists.
After commenting those three lines i have another error:
it says that myBrowserSelenium class does not have a getBrowser method which is expected by sfTestFunctionalBase.class.php.
Any clue?
Thanks
I followed the tutorial...
when i get to the "test:functional frontend home" part my output result is:
Fatal error: Class 'myBrowserSelenium' not found in SF_ROOT_DIR\lib\test\myBrowserFactory.class.php on line 11
What am i doing wrong?
Thanks.
http://thecodecentral.com/2010/08/25/selenium-and-symfony-integeration-the-non-plugin-approach-tutorial#header-3
i did as you told and everything seens to be like you describe..
i think that symfony is not including the classes in the 'SF_ROOT_DIR/lib/test'
I ran other tests with my lib/model classes, the autoload is fine and include all my lib/model classes, but it doesn't found the myBrowserFactory.
any ideas?
$b = new myBrowserSelenium(
sfConfig::get('app_domain'),
null,
array(),
array('browserType' => '*firefox')
);
the functional test crashs in the factory.
which indicates myBrowserSelenium not found, but myBrowserFactory is indeed loaded.
Please verify again which class is not loaded. From the error message you got, is it 'myBrowserSelenium not found' or 'myBrowserFactory not found'?
fopen(http://:/selenium-server/driver/?cmd=getNewBrowserSession&1=%2Achrome&2=http%3A%2F%2F): failed to open stream: Success in /opt/lampp/htdocs/project/trunk/lib/test/selenium/mySelenium.class.php on line 25
cannot connected to selenium server
Any thoughts how to resolve this. I removed the added (:/) in fopen that you can notice above... but then I started getting the following errors
PHP Warning: fopen(): php_network_getaddresses: getaddrinfo failed: Name or service not known in /opt/lampp/htdocs/project/trunk/lib/test/selenium/mySelenium.class.php on line 25
Thanks!