Preview of a new Selenium test suite
Some time ago I wrote about using Selenium with SimpleTest. It is a nice solution, but after working with it I had to experience that it doesn’t fit to my working style. I often forgot to start the Selenium server before running the tests, and as it is rather slow, it is not the ideal tool for doing TDD ;-)
So I had to go back to the Selenium helper. But somehow it felt no longer right to place the tests in app/views/pages/tests and to write them as thtml files. And so I wrote the Selenium test suite, which uses a different approach as you will see.
Let’s have a look at a simple example. Before we can start with the example, we have to install the test suite (please be aware that the test suite only works with the not yet released CakePHP 1.2):
- Download the Zip file from the download section
- Unpack the Zip file
- Add the following route to app/config/routes.php:
Router::connect('/selenium/*', array('controller' => 'selenium', 'action' => 'display'));
With that we are ready to start with the example. As the tests are organized in test suites, we start with such a test suite:
// app/tests/selenium/my_test_suite.php
class MyTestSuite extends SeleniumTestSuite {
var $title = 'My tests';
function execute() {
$this->addTestCase('Login', 'cases/LoginTest');
}
}
Each file placed in app/tests/selenium is considered to be a test suite, and must extend the class SeleniumTestSuite and implement the execute() function. This function is very simple: you just add the tests to the test suite.
The next step is to create our test case. A test case must extend SeleniumTestCase and, as a test suite, implement the execute() function. This function contains the test logic.
// app/tests/selenium/cases/login_test.php
class LoginTest extends SeleniumTestCase {
var $title = 'Login';
var $fixtures = array('Users');
function setUp() {
echo 'setUp';
}
function tearDown() {
echo 'tearDown';
}
function execute() {
$this->open('http://myproject.localhost');
$this->type('name=data[User][username]‘, $this->dho['username']);
$this->type(’name=data[User][password]‘, $this->dho['password']);
$this->clickAndWait(’submit’);
$this->verifyLocation(’http://myproject.localhost/projects’);
}
}
You probably noticed the $fixtures array. It allows you to write some records to the database before the test case is executed. These records are also available in the test case (see $this->dho). Each fixture is a simple class with the same name as the database table:
// app/tests/fixtures/users.php
class Users {
var $columns = array('id', 'username', 'password');
var $dho = array(1, 'dho', 'geheim');
}
With that we are ready to execute the test suite. As I use the Selenium IDE, I just have to enter the following url in Firefox to load the test suite:
chrome://selenium-ide/content/selenium/TestRunner.html?test=http://myproject.localhost/selenium/MyTestSuite
And the tests can be executed :)
Please keep in mind that it is just a preview, so it is possible that some things will change in future versions. As always your feedback is welcome :)




Selenium is only as slow as the computer you run it on. :)
I have found that my 2GHz Core Duo will happily run Selenium RC just as fast as FireFox will let it. Granted, I run the tests with ruby’s Test::unit, but I wouldn’t expect SimpleTest to have too much overhead.
As for as forgetting to launch the proxy server, I added a rake task that loads lighttpd, selenium, and opens my app directory in TextMate. Automation means never having to remember things. :)
Just a couple of notes in case anyone else wants to keep to the remote control path.
Hey Daniel: Thanks for this release, that’s pretty cool stuff. I actually just bought an older computer (~1ghz, 768mb ram, 20gb hdd) from a friend that I plan to use as a dedicated test server in the future. This should solve the performance issues for me ; ).
@Scott: Well, it is not Selenium that’s slow, it is the starting of Firefox which is slow (it takes more than 10 seconds on my machine). So that is a lot of overhead if your tests run for 5-10 seconds and you run the tests very frequently. But I am also aware that I am abusing Selenium a bit, as Selenium is afaik not thought to be used in a TDD way.
And yes, you are right with “Automation means never having to remember things”, I like that sentence. But sometimes I am just too lazy to automate something and then I prefer to complain ;-)
@Felix: Whoa, a dedicated test server is cool :)
[...] You probably know this fixture. It is the same I have used in the article about the Selenium test suite. This means you can reuse fixtures in your Selenium tests. [...]
I’m sorry. I am new to using selenium with PHP and Cake. I followed all the steps in your article and couldn’t get the selenium to work. When bringing up the test in firefox, the “Test Suite” are displays this:
// app/tests/selenium/my_test_suite.php class MyTestSuite extends SeleniumTestSuite { var $title = ‘My tests’; function execute() { $this->addTestCase(’Login’, ‘cases/LoginTest’); } }
and the “Current Test” is empty. Am I missing something?
Please can any body tell me where the test cases can be put to reflect in selenium ?
@Gururaj: Hm, I am not sure I understand your question. Do you ask where to put the test cases? They are in app/tests/selenium/cases.
HTH
hi folks ..
i have a doubt ,regarding automating application logins.
i have to write a test case ,so as to automate the process of login’s to an application .The login details can be taken from a file or directly from a database.
please do help me regarding this ..
regards
jannu
@jannu: With what do you need help?
Hi - I have a couple of diff’s to enhance your suite:
The first one just allows the ‘element’ type asserts to get rendered - I wanted to use a ‘verifyElementPresent’, and it wasn’t getting output in the test case.
The second one makes the test suite work if the cake application isn’t setup at the base of the server.
Thanks for all your tremendous contributions!
diff -r ./selenium_test_case.php /Volumes/yavin-1/var/www/sfl/vendors/selenium/selenium_test_case.php
97c97
$allowedKeywords = array(’alert’, ‘confirmation’, ‘element’, ‘prompt’, ‘text’);
diff -r ./selenium_test_suite.php /Volumes/yavin-1/var/www/sfl/vendors/selenium/selenium_test_suite.php
21c21,23
< echo ‘‘.$title.’‘;
—
> $dispatcher = new Dispatcher();
> $base = $dispatcher->baseUrl();
> echo ‘‘.$title.’‘;
@Rich: Thanks for the fixes. I have applied them and uploaded a new version.
Great post!
Here is a bit improved version that can insert multiple rows and fix to insert null values. File: selenium_test_case.php
function __getUrlForCommand($command, $folder, $testCaseName) {
return ‘http://’.$_SERVER['SERVER_NAME'].’/’.’selenium’.'/’.$command.’/’.$folder.’/’.$testCaseName;
}
function loadFixtures() {
if (!empty($this->fixtures)) {
$db =& ConnectionManager::getDataSource(’default’);
foreach ($this->fixtures as $fixture) {
require_once(APP.DS.’tests’.DS.’fixtures’.DS.Inflector::underscore($fixture).’.php’);
$f = new $fixture();
$variables = get_class_vars($fixture);
$db->execute(’DELETE FROM ‘.Inflector::underscore($fixture));
foreach ($variables as $name => $data) {
if ($name != ‘columns’) {
foreach ($f->$name as $state => $row) {
$this->$name = array_combine($f->columns, $row);
$values = array();
foreach ($row as $value) {
if (is_string($value)) {
$values[] = $db->value($value);
} elseif (is_bool($value)) {
$values[] = $value == true ? ‘true’ : ‘false’;
} elseif (empty($value)) {
$values[] = ‘NULL’;
} else {
$values[] = $value;
}
}
$sql = ‘INSERT INTO ‘.Inflector::underscore($fixture).’ (’.implode(’,', $f->columns).’) VALUES (’.implode(’,', $values).’)';
$db->execute($sql);
}
}
}
}
}
}
This will require to define fixture data as:
var $dho = array(
‘created’ => array(’1′,’user1′,’123′),
‘activated’ => array(’1′,’user2′,’456′)
);
and in the unit test you can access the data as $this->dho['activated']['username']
Oops, here is correct version of loadFixtures:
function loadFixtures() {
if (!empty($this->fixtures)) {
$db =& ConnectionManager::getDataSource(’default’);
foreach ($this->fixtures as $fixture) {
require_once(APP.DS.’tests’.DS.’fixtures’.DS.Inflector::underscore($fixture).’.php’);
$f = new $fixture();
$variables = get_class_vars($fixture);
$db->execute(’DELETE FROM ‘.Inflector::underscore($fixture));
foreach ($variables as $name => $data) {
if ($name != ‘columns’) {
$rows = array();
foreach ($f->$name as $state => $row) {
$rows[$state] = array_combine($f->columns, $row);
$values = array();
foreach ($row as $value) {
if (is_string($value)) {
$values[] = $db->value($value);
} elseif (is_bool($value)) {
$values[] = $value == true ? ‘true’ : ‘false’;
} elseif (empty($value)) {
$values[] = ‘NULL’;
} else {
$values[] = $value;
}
}
$sql = ‘INSERT INTO ‘.Inflector::underscore($fixture).’ (’.implode(’,', $f->columns).’) VALUES (’.implode(’,', $values).’)';
$db->execute($sql);
}
$this->$name = $rows;
}
}
}
}
}
@Rostislav: Thanks for your patch! I have to test it and will then integrate it.