Testing Simplified: Page Objects

Testing is hard. Maintaining tests is even harder.

When we first started using Selenium WebDriver, writing a test meant creating a one-off function that performed the given task, but duplicated a bunch of code, and used hard-coded elements if any HTML/CSS got involved. Trying to get coverage on something as large as the Behance Network (be.net) became bloated and unmaintainable.

Without an easy way to create and improve tests as features changed, QA + testing became the last priority in development.

As we upgraded our Network codebase from 2.0 to 3.0, we introduced tons of new features and revamped almost every old page. With everything being new, our existing tests had thousands of references to things that no longer existed.

Updating an element meant finding all references and modifying them in each location (potentially hundreds). This was entirely manual labor, and was extremely prone to error.
All of the one-off functions were conglomerated into one giant file, spanning 4-5k lines of code. When two developers needed to work on this file, massive conflicts were created during their subsequent merges.

“Page objects” were the solution to this growing problem. They were created as an Object-Oriented (OO) solution to abstract the actions of site pages into separate files. This changed:

public function deleteAccount( $password = '' ) {

  $this->open( '/' );
  $this->waitForElementPresent( 'primary-content', 'primary_content' );
  $this->waitForElementPresent( 'nav-account_network-privacy', 'account_settings' );
  $this->click( 'css=#nav-account_network a[href$="privacy"]' );
  $this->waitForElementPresent( 'delete_account-container', 'account_privacy' );

  $this->waitForBind( 'input#delete_account', 'click', 'delete_account' );
  $this->click( 'css=input#delete_account' );
  $this->waitForBind( '#delete-my-account', 'click', 'delete_my_account' );
  $this->click( 'delete-my-account' );
  $this->waitForElementPresent( 'css=div.popup-inner-wrap', 'popup' );
  $this->waitForElementPresent( 'delete-form', 'delete_form' );

  $this->type( 'password_confirm', $password );
  $this->click( 'popup-confirm' );
  $this->waitForElementPresent( 'gallery', 'gallery' );
} // deleteAccount

– to –

public function deleteAccount( $password = '' ) {

  return $this->open( 'Gallery Index' )
              ->visit( 'Account Settings' )
              ->visit( 'Privacy' )
              ->deleteAccount( $password );

} // deleteAccount

Under page objects, tests became a series of abstractable, reusable actions, instead of specific implementations. Developers (even those pesky front-enders who shy away from PHP) without heavy Selenium experience could now write maintainable tests for their pages.

Thanks to the magic of page objects, our test upgrades from Network 2.0 to 3.0 were completed within an hour.