Archive | February, 2014

SpinAsserts with Selenium

6 Feb

I have been doing a lot of Selenium-related stuff at work lately. For something that seems as simple as a macro “click this, click that, type this, type that”; it is actually not unlike writing development code.

The first big conceptual leap I had was page objects (https://code.google.com/p/selenium/wiki/PageObjects), that basically treated selenium scripts as modular pieces of code rather than a giant list of browser actions. It made me really appreciate OOP in programming (something that I took for granted while coding with frameworks).

The next big conceptual leap I had was spin asserts (http://saucelabs.com/resources/selenium/lose-races-and-win-at-selenium). It is a common problem that the selenium driver is over-eager to do what is asked of it, and gleefully fails at the speed of light. As my company’s web application depended heavily on AJAX, I would see false negatives from tests all the time. It got to the point where it was standard behavior to put a wait before a click action.

$this->waitForElementPresent("css=#identifier");
$this->click("css=#idenfifier");

It irked me to no end that this was necessary for proper test procedure, yet felt like unnecessary code duplication every time. Eventually I had enough. It was time to automate the writing of code.

 

At first, I was inspired by Saucelabs’ own version of spinAssert, from their Sausage library. However, it still required a lot of boilerplate to be written (https://github.com/jlipps/sausage#spinasserts).

public function testSubmitComments() {
    $comment = "This is a very insightful comment.";
    $this->byId('comments')->click();
    $this->keys($comment);
    $this->byId('submit')->submit();
    $driver = $this;

    $comment_test = function() use ($comment, $driver) {
        return ($driver->byId('your_comments')->text() == "Your comments: $comment");
    };

    $this->spinAssert("Comment never showed up!", $comment_test);
}

Which seemed like a very convoluted modification of

$this->assertText('comment', 'My comment');

The basic idea was pretty simple – if any selenium command failed, simply retry it. I tried to hook it up to __call, but PHPUnit was smarter than me and used reflection to circumvent my magic method. I briefly toyed with the idea of hooking into PHPUnit’s internals via decorators or listeners, but not only was it undocumented, unsupported and bizarre (I had never encountered the design pattern in the wild before) to begin with, they did not do what I wanted. Decorators only work at the start and end of a test run, and listeners merely collect data. The last option left to me was a wrapper method.

I had much resistance against a wrapper method, as it was non-native and would be an additional learning curve over the standard Selenium functions, but short of modifying PHPUnit’s internals, that was the best I could do after days of research. Fortunately, it turned out to be pretty simple to code and not so hard to write.

This is what I had:

public function spinAssert() {
    $args = func_get_args();
    $command = array_shift($args);

    call_user_func_array(array($this, "waitFor" . $command), $args);
    call_user_func_array(array($this, "assert" . $command), $args);
}

and this was how I called it:

$this->spinAssert('Text', 'comment', 'This is a comment');

Thankfully, PHPUnit already provided implicit waits by virtue of the “waitFor” family of functions, which does the whole loop of send command, check for success, sleep(1); if not and retry. As such, it was pretty straightforward to implement.

Advertisements