When writing automated acceptance Tests with Tools like Selenium it becomes handy to abstract from the concrete page by using a Pattern like "Window Driver" or "Page Object".
The goal is, that the acceptance tests are written in a domain specific language - without knowing detail about the page markup.
Example 1 - Login Page
Lets say you want to test a login page. You can do this like:
/**
*
* @test
*/
public function canLogin() {
$this->open('/login');
$this->type ( 'username', $login );
$this->type ( 'password', $password );
$this->clickAndWait ( "//input[@id='submit' and @name='submit' and @value='submit']" );
$this->assertTextPresent('Username or Password wrong');
$this->type ( 'username', $login );
$this->type ( 'password', $password );
$this->clickAndWait ( "//input[@id='submit' and @name='submit' and @value='submit']" );
$this->assertTextPresent('Login ok');
}
But better would be something like this:
/**
*
* @test
*/
public function canLogin() {
$loginPage = new LoginPage($this);
$loginPage->loginAs('testuser','wrongpassword');
$this->assertContains('Username or Password wrong', $loginPage->getErrorMessage());
$loginPage->loginAs('testuser','testpassword');
$this->assertTrue($loginPage->isLoggedIn());
}
Example2 - Search Page
Lets think of a second example: A search page that shows some results. The search page has a pagination and sort options. We want to test if the pagination and the sorting works.
Using a Page Object for the Search a test could look something like this:
/**
* @test
*/
public function articleSearchWorks() {
$search = new Acceptance_Driver_SearchPage($this);
$search->open('/index.php?id=502');
//check if the searchpage has a pagination
$this->assertTrue($search->hasPagination());
//get the first resultitem that is shown:
$firstResultLink = $search->getResultDetailLink(1);
//Now click on sorting - to sort for oldest results first
$search->selectSorting('Oldest');
$firstResultLinkAfterSort = $search->getResultDetailLink(1);
//Check that the Results has changed and does not show the same
$this->assertNotEquals($firstResultLink, $firstResultLinkAfterSort);
//now click on page 2 in pagination
$search->gotoPage(2);
$this->assertEquals('2',$search->getCurrentPage());
$firstResultLinkOnPage2 = $search->getResultDetailLink(1);
//Check that the result shown on page 2 is diffrent
$this->assertNotEquals($firstResultLinkOnPage2, $firstResultLinkAfterSort);
}
The Window Driver (or Page Object) that belongs to the search page can look like this:
class Acceptance_Driver_SearchPage {
/**
* @var TestingFramework_Server_SeleniumTestCase
*/
private $testCase;
/**
* @param TestingFramework_Server_SeleniumTestCase $testCase
*/
public function __construct(TestingFramework_Server_SeleniumTestCase $testCase) {
$this->testCase = $testCase;
}
/**
* opens a search page (and verify that this is a search page)
* @param string $url
*/
public function open($url) {
$this->testCase->open($url);
$this->testCase->assertElementPresent('//div[@class="def-resources"]');
}
/**
* @return boolean
*/
public function hasPagination() {
return $this->testCase->isElementPresent ( '//ul[@class="paginator"]/li[@class="current"]');
}
/**
* returns the detaillink of the result on the specified position
* @param integer $pos
* @return string
*/
public function getResultDetailLink($pos) {
try {
return $this->testCase->getAttribute('//ul[@class="article-list"]/li['.$pos.']//a@href');
}
catch (Exception $e) {
throw new Exception('No First Result Found');
}
}
/**
* @return integer
*/
public function getPaginationPageCount() {
return $this->testCase->getXpathCount('//ul[@class="paginator"]/li');
}
/**
* returns current active page in pagination
* @return string
*/
public function getCurrentPage() {
return $this->testCase->getText('//ul[@class="paginator"]/li[@class="current"]');
}
/**
* clicks on the pagination
* @param integer $page
*/
public function gotoPage($page) {
$this->testCase->clickAndWait('link='.$page);
}
/**
* clicks on the sorting
* @param string $name
*/
public function selectSorting($name) {
$this->testCase->clickAndWait('link='.$name);
}
}
Summary
To summarize:
- Use seperate Classes to abstract from certain pages or windows in your application
- Write Tests using the domain specific language that this objects provide you.
- If the markup or layout changes, you only need to change the PageObject.
- Asserts should happen in the Test (not in the pageobject)
- Selenium gives you useful functions to get the result and use them in your tests, like:
- getText(<idendifier>)
- getAttribute(<idendifier>@<attributname>)
- getXpathCount(<xpath>)
- getHtmlSource() (to get complete HTML)
- getCookieByName() ....
I also found a good Google Article about that pattern: code.google.com/p/selenium/wiki/PageObjects
