Running SauceLabs Selenium test suite locally with PHPUnit

SauceLabs provide a great hosted Selenium service for cross-browser testing in the cloud, which allows you to run a Selenium test suite against multiple browsers using the SauceLabs API and SauceConnect.

They also provide integration with PHPUnit, so that you can plug your SauceLabs Selenium tests directly into your PHPUnit test suite.

Sometimes, though, you want to run the same Selenium test suite against a single browser on your local machine, before you run the full thing against all the browsers on SauceLabs.

However, the SauceLabs PHPUnit integration requires you to extend their special base class, which they’ve added to their custom PHPUnit library (which you need to have installed instead of the standard PHPUnit). That base class assumes that the test suite is always going to run against the remote SauceLabs API.

To run a Selenium test suite locally, you need to be extending the standard PHPUnit Selenium base class instead of the custom SauceLabs one – but you don’t want to duplicate all your tests in two class hierarchies.

How can you have a class where the parent class can be one of two different things, depending on some config setting?

The answer is by using  PHPs “class_alias” and a custom autoloader (note, “class_alias” is only available in PHP from 5.3.0 onwards). It works something like this..

Instead of extending either the special SauceLabs base class or the PHPUnit selenium base class directly, you define two different intermediate base classes – called RemoteSeleniumTestCase and LocalSeleniumTestCase – which each extend one of the base classes:

// RemoteSeleniumTestCase.php
class RemoteSeleniumTestCase extends PHPUnit_Extensions_SeleniumTestCase_SauceOnDemandTestCase 
{
    function setUp() {..}    // special setup for SauceLabs 
}
// LocalSeleniumTestCase.php
class LocalSeleniumTestCase extends PHPUnit_Extensions_SeleniumTestCase
{
    function setUp() {..}    // standard setup for local Selenium
}

The your actual test suite extends a (virtual) class – called DynamicSeleniumTestCase:

// your selenium test cases
class HomeTest extends DynamicSeleniumTestCase
{
    function testHomePage()
    {
        // check home page is there
        $this->open('/');
        $this->assertTextPresent('Welcome to BigCo');
   } 
}

So, where does DynamicSeleniumTestCase come from?

That’s where class_alias and the autoloader come in – you define a custom autoloader, which will alias DynamicSeleniumTestCase to one or other of your intermediate base classes, based on the value of an environment variable (I’ve used RUN_SELENIUM_REMOTE):

spl_autoload_register(function($class) {
    if (strcasecmp($class, 'DynamicSeleniumTestCase') === 0) {
        $remote = getenv('RUN_SELENIUM_REMOTE');
        if ($remote) {
            require_once 'RemoteSeleniumTestCase.php';
            class_alias('RemoteSeleniumTestCase', 'DynamicSeleniumTestCase');
        } else {
            require_once 'LocalSeleniumTestCase.php';
            class_alias('LocalSeleniumTestCase', 'DynamicSeleniumTestCase');
        }
    }
}, true, true);

Then, to make your test suite run against your local Selenium setup and local browser, just do

export RUN_SELENIUM_REMOTE=0

before firing off the build, and to run the same thing against the SauceLabs server, do

export RUN_SELENIUM_REMOTE=1

Genius or insanity?! You decide.

 

PhpUnit Mocks that suddenly stopped working

We came across a strange thing with the mock framework in PhpUnit .

We had some test code that created a mock for a Store class, and gave it a method to mock out:

        $mockStore = $this->getMock(‘Store’, array(‘save’));

That seemed to work fine in the tests, we could setup expectations, the correct return values got returned, and so on.

Then, in some unrelated code, we changed some require_once statements, and suddenly the test with the mocks stopped working – it threw exceptions trying to create the Store mock. 

It turns out that the constructor for the Store class needed some parameters, and the mock framework needs you to supply those parameters when you create the mock, because it will call the real constructor behind the scenes.

The mock had been working OK previously because the real Store class hadn’t been loaded anywhere by the test – but then the changes to some require_once statements elsewhere in the code meant that the Store class WAS now loaded, and instantiated by the call to create a mock.

 

So the mock framework in PhpUnit will create a mock for you even if it has no idea what class it is that you’re trying to mock out – you could do

        $mockStore = $this->getMock(‘foo’, array(‘save’));

and it would still give you a mock that worked fine. If it DOES find a class of that name, though, it’ll instantiate it.

In fact, this behaviour is probably a good thing for TDD – it means you can create the mocks you need before you’ve ever created the real classes. You just have to be aware that when you DO create the real class, the mocks will start being created based on the real thing.

 

The solution in our test was to use the flag that tells the mock framework not to call the original constructor – it’s a bit more clumsy, because you also have to supply some other additional parameters:

        $mockStore = $this->getMock(‘Store’, array(‘save’), array(), ”, FALSE);