Functional testing with PhantomJS, Selenium and PHPUnit

TL;DR

GitHub repo with provisioning, README and unit tests: https://github.com/ashleyhindle/phpunit-selenium-phantomjs-example

How to setup Selenium & PhantomJS

For my test purposes I use Selenium Standalone Server; this is a Java application so requires: apt-get install openjdk-7-jre-headless

PhantomJS is a single binary with no requirements (pretty awesome), which can be ran as a headless browser or with WebDriver.

During my setup I had numerous issues/bugs using PhantomJS WebDriver (it uses GhostDriver) with Selenium, and don't currently recommend you try it this way.

As we'll be using PhantomJS without WebDriver we only need one command to get Selenium up and running: java -jar /usr/local/bin/selenium-server-standalone-2.53.1.jar -log /tmp/selenium.log -Dphantomjs.binary.path=/usr/local/bin/phantomjs

Each of our tests will connect to Selenium and post messages on how to handle the configured browser (phantomjs in our case, other brands are available).

How to setup PHPUnit

Your composer.json will require phpunit/phpunit and phpunit/phpunit-selenium to run tests with Selenium.

This is the composer.json file I use:

{
    "require-dev": {
    "php": ">=5.6",
        "phpunit/phpunit": "5.4.8",
        "phpunit/phpunit-selenium": "3.0.2"
    },
    "scripts": {
        "test": "vendor/bin/phpunit"
    }
}

All unit tests will need to extend PHPUnit_Extensions_Selenium2TestCase and requires a setUp method.

Here is the setUp method I use:

public function setUp() {  
    $this->setHost('192.168.50.50'); // Vagrant private IP, so we can run the tests from our local machine
    $this->setPort(4444); // Port selenium listens on

    $this->setBrowser('phantomjs'); // Browser to use (you can use Chrome/Firefox if you set those drivers up separately)

    $this->setBrowserUrl('https://twitter.com'); // Base URL I want to test.  This would be your project's dev URL

    $this->prepareSession()->currentWindow()->size(array(
        'width' => 1024,
        'height' => 768,
    ));
}

As most sites use responsive CSS I explicitly set the width and height of my window, to ensure that it doesn't hide search boxes and the like behind the hamburger menu.

Hidden elements are not accessible from Selenium.

How to write tests

All tests should extend PHPUnit_Extensions_Selenium2TestCase, and have a setUp method. Currently I also always use tearDown method to ensure resources are cleaned up and we're good to go:

public function tearDown() {  
    $this->closeWindow();
}

Most time writing these tests is finding the methods you need to get what you want, so here's a cheatsheet:

MethodResultNotes
$this->url('/cheeseBurger');Navigates the browser to that URLIf you don't pass a value in, it returns the current URL
$this->byCssSelector('.Button.StreamSignup.js-nav.js-signup');Returns 1 element via CSS selectors. #id, tagName, .classThrows PHPUnit_Extensions_Selenium2TestCase_WebDriverException if it's not found
$this->byXPath('//div[@attr="value"]');Returns 1 element by XPathReturns an instance of PHPUnit_Extensions_Selenium2TestCase_Element
$this->byXxx('value');Returns 1 element based on the selector: byId, byClassName, byLinkText, byPartialLinkText, byTag, byNameReturns an instance of PHPUnit_Extensions_Selenium2TestCase_Element
$inputElement->value('cheese');Sets an elements value; usually used for inputs returned from one of the above selectorsReturns the value of the element
$element->text();Get the visible text of an element, not the inner HTMLUsed with an element from a selector; I usually use it with $this->assertEquals('Sign up', $element->text());
$element->submit();Submits a form I usually use this on an input box retrieved with $this->byId('searchInput');
$element->click();Clicks an element Doesn't have to be an anchor tag. Can be a div which expands a dropdown
$this->prepareSession()->currentWindow()Get the current window objectInstance of PHPUnit_Extensions_Selenium2TestCase_Window
$this->waitUntil(function(){}, maxTimeoutInMilliseconds)Wait until this closure doesn't return nullThis is useful for AJAX. You can use $element->click() then waitUntil and byCssSelector to wait until it's ready. Example

There are a lot more useful methods for testing with AJAX and Javascript, and I'll make sure to update this post as I get time to add them.

Function to save screenshots

private function screenshot($filename){  
    $filedata = $this->currentScreenshot();
    file_put_contents($filename, $filedata);
}

...

$this->screenshot(__DIR__ . '/screenshots/screenshot-after-search.png');

All together now

You can see a PHPUnit test case using most of these methods in my GitHub repo for this blog post.

Resources

Tests for the test case we extend are invaluable for seeing what's possible and available: https://github.com/giorgiosironi/phpunit-selenium/blob/master/Tests/Selenium2TestCaseTest.php

Automated documentation for the test case we extend is very useful: http://apigen.juzna.cz/doc/sebastianbergmann/phpunit-selenium/class-PHPUnitExtensionsSelenium2TestCase.html