Before Setup

Step One of an xUnit style script is the Setup method gets called. In Se scripts, this is the logical place to put in browser creation, but what if you wanted to make some sort of determination before launching the browser? If you had just a single script it wouldn’t be much of a challenge, but if you are some inheritance thrown into the mix it becomes a bit more interesting.

Scenario: You are automating a site that has dynamic ‘modules’ based on the whims of a ‘content’ team. Loading a browser is expensive so you want to avoid that cost if the particular page does not have the ‘module’ that the script exercises.

Here is the is what my first, expensive, solution was for dealing with whether or not a module was on the page

public class CustomTestCase extends PHPUnit_Framework_TestCase {
    public function setUp() {
        $this->selenium = SaunterPHP_Framework_SeleniumConnection::getInstance()->selenium;
        $this->selenium->start();
        $this->selenium->windowMaximize();
 
        $this->sessionId = self::$selenium->getEval("selenium.sessionId");
    }
}
public class FlyingMonkeyTest extends CustomTestCase {
    public function setUp() {
      parent::setUp();
    }
 
    /**
    * @test
    * @group shallow
    */
    public function happy_registration() {
      $home = new HomePage();
      if (! home->has_the_module_i_want()) {
          $this->markTestSkipped('No modules of this type currently on page');
      }
      // do stuff
 
      // assert stuff
    }
}

If the creation of a browser, loading of the page and then determination of whether the script should proceed (or be skipped) only took 20 seconds and there are (say) 20 page and module combinations that need to be skipped than that is just over 6.5 minutes per run that are being wasted.

Solving this problem takes a bit of magic and a whole lot of familiarity with how your runner works. (You do have the source code to your runner, right? You know, so you know what it is that is going under the hood in just these very scenarios.) By the time setUp is called, the runner needs to know all about the method it will run immediately after. And so we can make use of this knowledge and add some meta-data to our test method in the doc strings.

Here is what my second approach looked like. The parseAnnotations function is lifted exactly from the PHPUnit source. It is a private method and I couldn’t figure out how to get at it directly. I might have if I spent more time but I was already pretty deep down a rabbit hole. The other method you’ll see a bit more of in a second.

public class CustomTestCase extends PHPUnit_Framework_TestCase {
    public function setUp() {
        $this->selenium = SaunterPHP_Framework_SeleniumConnection::getInstance()->selenium;
        $this->selenium->start();
        $this->selenium->windowMaximize();
 
        $this->sessionId = self::$selenium->getEval("selenium.sessionId");
    }
 
    protected function parseAnnotations($docblock)
    {
        $annotations = array();
 
        if (preg_match_all('/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m', $docblock, $matches)) {
            $numMatches = count($matches[0]);
 
            for ($i = 0; $i < $numMatches; ++$i) {
                $annotations[$matches['name'][$i]][] = $matches['value'][$i];
            }
        }
 
        return $annotations;
    }
 
    protected function check_for_url_regex_precondition() {
        // it is crazy inefficient to open a browser when the module we are checking doesn't exist on it
        $reflector = new ReflectionMethod($this, $this->getName());
        $this->annotations = $this->parseAnnotations($reflector->getDocComment());
 
        if (array_key_exists("precondition", $this->annotations)) {
            foreach ($this->annotations["precondition"] as $condition) {
                if (startsWith($condition, "url")) {
                    $url = substr($condition, 4);
                }
 
                if (startsWith($condition, "regex")) {
                    $pattern = substr($condition, 6);
                }                
            }
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $GLOBALS['settings']['webserver'] . $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            $page = curl_exec($ch);
            if (! preg_match('/' . $pattern . '/', $page)) {
                $this->markTestSkipped('No stories of this type currently');
            }
            curl_close($ch);
        }
    }
}

With that in place we can add a method into our setUp() before the parent one is called. If you read the code above you’ll see that it parses the documentation annotations for any that start with precondition and then uses curl to fetch the page and then regexes it for the provided pattern. If it is not found then the test is marked as skipped — before the browser is ever opened.

public class FlyingMonkeyTest extends CustomTestCase {
    public function setUp() {
        $this->check_for_url_regex_precondition();
        parent::setUp();
    }
 
    /**
    * @test
    * @group shallow
    * @precondition url:/Flying-Monkey
    * @precondition regex:Link-List-\d
    */
    public function happy_registration() {
        $home = new HomePage();
        if (! home->has_the_module_i_want()) {
            $this->markTestSkipped('No modules of this type currently on page');
        }
        // do stuff
 
        // assert stuff
    }
}

There are all sorts of situations where this wouldn’t work so well (having to authenticate in a custom manner comes to mind immediately) but for this particular situation its working like a charm. Oh. If you are also using something like Sauce Labs OnDemand the savings are also measurable in dollars not just minutes.

Post a Comment

Your email is never published nor shared. Required fields are marked *