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
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
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.
What is Non-Brittle
Reusable
Easily updated
Modular
Extensible
Not broken by minor page updates
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