news, pattern

Window Driver Pattern for Acceptance Tests

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

 

blog comments powered by Disqus
blogroll