UI Testing with Selenium in PHP

How to use User Interface testing to increase product quality and decrease support costs.

Benjamin Cool / @benjamincool

Benjamin Cool

  • Application Optimizer at A2 Hosting
  • M.S. in Computer Science
  • Concentration in Artificial Intelligence
  • Member of the awesome WebDev team at A2 Hosting
  • Follow me @benjamincool for slides

Takeaways

  • UI testing is important
  • UI testing is valuable for your business
  • Brittle testing methods are no good
  • The Page Object Model is better
  • Testing can be done in PHP

Business Value

Acceptance Testing

  • The project meets the clients needs
  • Each element of the project meets acceptance criteria
  • Helps develop clear communication of needs

Reduce Time "Bug Fixing"

  • Tests are run any time new code is developed
  • Allows for catching potential problems early
  • Code does not go live without passing tests.

Increase Overall Product Quality

  • The product meets the specs defined
  • Improves project workflow
  • Forces higher quality coding practices
  • Improves customer confidence

Value to Development Teams

  • Again, less time fixing bugs
  • Protects from collateral damage
  • Forces better communication of customer needs
  • Developers can PROVE code works
  • lowers stress

Brittle testing methods

Screen Capture / Recorders

  • Selenium IDE (firefox plugin)
  • Ghost Inspector

How screen Capture tests work

  • Open a web page
  • start recording
  • click around and fill out forms
  • stop recording
  • run the test against multiple browsers

What screen capture based tests are good for

  • quick testing
  • pages that will never, ever change
  • proof of concepts

What screen capture based tests are NOT good for

  • robust testing
  • dynamic pages
  • pages under development
  • pages that will change over time
  • surviving a redesign

Selenium WebDriver Primer

    Common languages like PHP, JavaScript, Java and C#
    installable using composer
    Used with PHPUnit for assertions
    Used for robust testing suites

PHP WebDriver Common Functions

  • WebDriver Class
    • get()
    • FindElements()
    • FindElement()
    • getTitle()

WebDriverElement Functions

  • FindElements()
  • FindElement()
  • getAttribute()
  • getCSSValue()
  • getLocation()
  • getSize()

WebDriverElement Functions Continued

  • getTagName()
  • isEnabled()
  • isSelected()
  • submit()
  • clear()

WebDriverElement Functions Continued

  • click()
  • isDisplayed()
  • sendKeys()
  • getText()
  • getID()
  • getLocationOnScreenOnceScolledIntoView()

FindElement and WebDriverBy::

  • id()
  • className()
  • cssSelector()
  • xpath()
  • tagName()
  • name()
  • linkText()

Brittle WebDriver test in PHPUnit

class UITestWPExample extends PHPUnit_Framework_TestCase {
 ...
 public function setUp(){
  $capabilities = array(\WebDriverCapabilityType::BROWSER_NAME => 'firefox');
  $this->driver = RemoteWebDriver::create('http://127.0.0.1:4444/wd/hub',$capabilities);
  $this->driver->get('http://local.wordpress.dev/wp-login.php');
 }
 ...
}
class UITestWPExample extends PHPUnit_Framework_TestCase {
 protected $driver;
 
 public function canLogInAndSeeHelloDolly() {
  $driver = $this->driver;
  $username   = $driver->findElement(WebDriverBy::id('user_login'));
  $password   = $driver->findElement(WebDriverBy::id('user_pass'));
  $rememberme = $driver->findElement(WebDriverBy::id('rememberme'));
  $submit     = $driver->findElement(WebDriverBy::id('wp-submit'));
  $username->sendText('admin');
  $password->sendText('password');
  $rememberme->click();
  $submit->click();
  $admin_url = 'http://local.wordpress.dev/wp-admin/index.php';
  $driver->wait(5,500)->until(
   function () use ($driver) {
    return $driver->getCurrentURL() == $admin_url;
   },
   '5 seconds passed page did not load. oh noes.'
  );
  $this->assertEquals($admin_url,$driver->getCurrentURL());
  $this->assertTrue($driver->findElement(WebDriverBy::id('dolly'))->isDisplayed());
 }
}
class UITestWPExample extends PHPUnit_Framework_TestCase {
 ...
 ...
 ...
 public function canLogIn() {
   $driver = $this->driver;
  $username   = $driver->findElement(WebDriverBy::id('user_login'));
  $password   = $driver->findElement(WebDriverBy::id('user_pass'));
  $rememberme = $driver->findElement(WebDriverBy::id('rememberme'));
  $submit     = $driver->findElement(WebDriverBy::id('wp-submit'));
  $username->sendText('admin');
  $password->sendText('password');
  $rememberme->click();
  $submit->click();
  $admin_url = 'http://local.wordpress.dev/wp-admin/index.php';
  $driver->wait(5,500)->until(
   function () use ($driver) {
    return $driver->getCurrentURL() == $admin_url;
   },
   '5 seconds passed page did not load. oh noes.'
  );
  $this->assertEquals($admin_url,$driver->getCurrentURL(),'not logged in');
 }
}

There has to be a better way!

  • Element IDs can change.
  • Log in process can change.
  • Same code being used over and over again.
  • Same code being used over and over again.

Non-Brittle Tests

What is Non-Brittle

Reusable
Easily updated
Modular
Extensible
Not broken by minor page updates

Page Object Model

Why We Use the Page Object Model

Separate tests from logic to find page elements
Define reusable objects for elements on the page
Reduces code duplication
Increases test readability

Separating Finding Page Elements From the Test

Create objects in PHP
Well named functions
Test does not use the WebDriver to find elements
Test only asserts true or false or equal to

Page Object Model Basics

Break the page into distinct components
Write an object or trait for each component
Object returns what test needs to assert
Objects do not assert
class WPPage {
  public $driver, $site_url, $login_url, $admin_url;

  public function __construct($driver){
    $this->driver = $driver;
    $this->site_url = "http://local.wordpress.dev";
    $this->login_url = "{$this->site_url}/wp-login.php";
    $this->admin_url = "{$this->site_url}/wp-admin/";
  }
  
  public function get_admin_url(){
    return $this->admin_url;
  }
  
  public function getCurrentURL(){
    return $this->driver->getCurrentURL();
  }
  
  public function is_hello_dolly_visible(){
    return $this->driver->findElement(WebDriverBy::id('dolly'))->isDisplayed();
  }
  
  public function login(){
    try{
      $this->driver->get($this->login_url);
      $this->driver->findElement(WebDriverBy::id('user_login'))->sendKeys('admin');
      $this->driver->findElement(WebDriverBy::id('user_pass'))->sendKeys('password');
      $this->driver->findElement(WebDriverBy::id('rememberme'))->click();
      $this->driver->findElement(WebDriverBy::id('wp-submit'))->click();
        $this->driver->wait(5,500)->until(
         function ()  {
          return $this->driver->getCurrentURL() == $this->admin_url;
         },
         '5 seconds passed page did not load. oh noes.'
        );
      }
    catch (Exception $e){
      return false;
    }
    return true;
  }
}
class WPPage {
  public $driver, $site_url, $login_url, $admin_url;

  public function __construct($driver){
    $this->driver = $driver;
    $this->site_url = "http://local.wordpress.dev";
    $this->login_url = "{$this->site_url}/wp-login.php";
    $this->admin_url = "{$this->site_url}/wp-admin/";
  }
  
  ...
}
class WPPage {
  ...
  
  public function get_admin_url(){
    return $this->admin_url;
  }
  
  ...
}
class WPPage {
  ...
  
  public function is_hello_dolly_visible(){
    return $this->driver->findElement(WebDriverBy::id('dolly'))->isDisplayed();
  }
  
  ...
}
class WPPage {
  ...
  
  public function login(){
    try{
      $this->driver->get($this->login_url);
      $this->driver->findElement(WebDriverBy::id('user_login'))->sendKeys('admin');
      $this->driver->findElement(WebDriverBy::id('user_pass'))->sendKeys('password');
      $this->driver->findElement(WebDriverBy::id('rememberme'))->click();
      $this->driver->findElement(WebDriverBy::id('wp-submit'))->click();
        $this->driver->wait(5,500)->until(
         function ()  {
          return $this->driver->getCurrentURL() == $this->admin_url;
         },
         '5 seconds passed page did not load. oh noes.'
        );
      }
    catch (Exception $e){
      return false;
    }
    return true;
  }
}
    $this->driver->findElement(WebDriverBy::id('user_login'))->sendKeys('admin');
    $this->driver->findElement(WebDriverBy::id('user_pass'))->sendKeys('password');
    $this->driver->findElement(WebDriverBy::id('rememberme'))->click();
    $this->driver->findElement(WebDriverBy::id('wp-submit'))->click();
public function login_enter_user_login($keys){
	$this->driver->findElement(WebDriverBy::id('user_login'))->sendKeys($keys);
}

public function login_enter_user_pass($keys){
	$this->driver->findElement(WebDriverBy::id('user_pass'))->sendKeys($keys);
}

public function login_click_rememberme(){
	$this->driver->findElement(WebDriverBy::id('wp-submit'))->click();
}

public function login_click_submit(){
	$this->driver->findElement(WebDriverBy::id('wp-submit'))->click();
}
    $this->login_enter_user_login('admin');
    $this->login_enter_user_pass('password');
    $this->login_click_remember_me();
    $this->login_click_submit();
    $this->driver->wait(5,500)->until(
     function ()  {
      return $this->driver->getCurrentURL() == $this->admin_url;
     },
     '5 seconds passed page did not load. oh noes.'
    );
class WPAdminPage {
 protected $driver;
 public __construct($driver){
  $this->driver = $driver;
 }
 
 public function isHelloDollyVisible(){
  try{
    $dolly = $this->driver->findElement(WebDriverBy::id('dolly'));
    return $dolly->isDisplayed();
  }
  catch(NoSuchElementException $e){
    return false;
  }
 }
}
class UITestWPExample extends PHPUnit_Framework_TestCase {
 protected $driver;
 public function setUp(){
  $capabilities = array(\WebDriverCapabilityType::BROWSER_NAME => 'firefox');
  $this->driver = RemoteWebDriver::create('http://selenium_server',$capabilities);
  $this->driver->get('http://local.wordpress.dev/wp-login.php');
 }
 public function testCanLogIn() {
  $page = new WPLoginPage($this->driver);
  $page->logIn();
  $page = new WPAdminPage($this->driver);
  $this->assertEquals($page->get_admin_url(),$page->getCurrentURL());
 }
 public function testCanLogInAndSeeHelloDolly() {
  $page = new WPAdminPage($this->driver);
  $page->logIn();
  $page = new WPAdminPage($this->driver);
  $this->assertEquals($page->get_admin_url(),$page->getCurrentURL());
  $this->assertTrue($page->isHelloDollyVisible());
 }
}
class UITestWPExample extends PHPUnit_Framework_TestCase {
 ...
 public function setUp(){
  $capabilities = array(\WebDriverCapabilityType::BROWSER_NAME => 'firefox');
  $this->driver = RemoteWebDriver::create('http://selenium_server',$capabilities);
  $this->driver->get('http://local.wordpress.dev/wp-login.php');
 }
 ...
}
class UITestWPExample extends PHPUnit_Framework_TestCase {
 ...
 
 public function testCanLogIn() {
  $page = new WPLoginPage($this->driver);
  $page->logIn();
  $page = new WPAdminPage($this->driver);
  $this->assertEquals($page->get_admin_url(),$page->getCurrentURL());
 }
 
 ...
}
class UITestWPExample extends PHPUnit_Framework_TestCase {
 ...
 
 public function testCanLogInAndSeeHelloDolly() {
  $page = new WPAdminPage($this->driver);
  $page->login();
  $page = new WPAdminPage($this->driver);
  $this->assertEquals($page->get_admin_url(),$page->getCurrentURL());
  $this->assertTrue($page->isHelloDollyVisible());
 }
 
 ...
}

Testing for Accessibility

class UITestWPExample extends PHPUnit_Framework_TestCase {
 ...
 ...
 ...
 public function testButtonsHaveAriaRoles(){
  $page = new HomePage($this->driver);
  $buttons = $page->getButtons();
  foreach($buttons as $button){
    assertTrue($button->hasAttribute('aria-role'));
  }
 }
}

The Selenium Server

  • Built in FireFox support
  • Automates the browser
  • Listens to commands from the Test
  • 3rd party Drivers made by browser vendors

Selenium Server Services

  • BrowserStack
  • Sauce Labs
  • Cross Browser Testing

Advantages of using a service

  • Up to date WebDrivers
  • Multiple Operating Systems
  • Mobile Devices
  • Parallel Tests

What is Needed

  • Development Environment with PHP 5.6+
  • PHPUnit
  • PHP WebDriver by Facebook
  • Selenium Server (Java) or Service

Installing PHPUnit and PHP Webdriver

Use composer for the easiest method

  {
    "require-dev" : {
      "phpunit/phpunit"    : "5.3.*",
      "facebook/webdriver" : "dev-master"
    }
  }

Using Selenium Standalone Server

  • Download from seleniumhq.org
  • java -jar selenium-server-standalone-2.53.0.jar
  • http://127.0.0.1:4444/wd/hub

Running a test on your local machine

Thank You!

Follow me on Twitter
@benjamincool for slides