Writing Selenium Tests with Node (the Enjoyable Way)

TL;DR - Grab this repo. It uses "fibers" to let you write Selenium tests in JavaScript without all of the extra ceremony required by callback functions or promise chains.

Before we go any further, allow me to get this out of the way: JavaScript is awesome, ok? There - I said it. The asynchronous, prototype-based nature of the language makes it an invaluable tool for any software developer's toolbelt. Best of all, with the advent of Node.js, opportunities for its application run the gamut from simple browser-based apps to complex, scalable, enterprise-ready server applications. As LeVar Burton would say, "... but you don't have to take my word for it..." - just ask PayPal.

"JavaScript is awesome. But it also kind of sucks."

That being said, as with most things in life, there's a "but." You knew there was a "but," right? Is JavaScript awesome? Yes. BUT - it also kind of sucks. The asynchronous nature of the language, one of its core strengths, is also its Achille's heel.

I was prompted to write this post by a recent need to develop Selenium integration tests within Node. If you're unfamiliar with Selenium, it's an open-source platform that allows you to write tests that mimic real-world user interactions within a web application. Selenium accomplishes this by creating instances of real web browsers, such as Chrome, Firefox, and Internet Exploder Explorer. The tests that are written for Selenium instruct these browsers to perform one or more actions (e.g. "click this button," "fill out this form field"), at which point a certain result is verified to meet some pre-defined criteria.

The automated feedback provided by Selenium is quite useful, but if you're going to make extensive use of it, you should take great pains to ensure that your tests are both easy to write and easy to maintain. Fail to do so, and you'll quickly find yourself spending a majority of your time tweaking tests and a minority of your time actually creating a product.

Selenium Tests with Node - The Painful Way

One of the more popular libraries for developing Selenium tests within Node provides two primary means by which you can write your tests:

The Asynchronous Approach

browser.init({browserName:'chrome'}, function() {  
    browser.get("http://admc.io/wd/test-pages/guinea-pig.html", function() {
        browser.title(function(err, title) {
            title.should.include('WD');
            browser.elementById('i am a link', function(err, el) {
                browser.clickElement(el, function() {
                    browser.eval("window.location.href", function(err, href) {
                        href.should.include('guinea-pig2');
                        browser.quit();
                    });
                });
            });
        });
    });
});

This is the definition of callback hell. Don't do this.

The "Chained Promises" Approach

browser  
    .init({browserName:'chrome'})
    .get("http://admc.io/wd/test-pages/guinea-pig.html")
    .title()
        .should.become('WD Tests')
    .elementById('i am a link')
    .click()
    .eval("window.location.href")
        .should.eventually.include('guinea-pig2')
    .back()
    .elementByCss('#comments').type('Bonjour!')
    .getValue().should.become('Bonjour!')
    .fin(function() { return browser.quit(); })
    .done();

"Chained promises that extend beyond two or three levels are just ridiculous."

JavaScript Promises are an incredibly useful mechanism for taming ridiculously nested callbacks (i.e. "the pyramid of doom"). But I'm going to go out on a limb and say that what we're looking at here is almost just as bad. Chained promises that extend beyond two or three levels are just ridiculous. Is it functional? Yes. Is it better than the previous example? Maybe. Is it easily readable and maintainable? No.

Code like this is giving sane JavaScript developers a collective migraine. Cut it out.

Selenium Tests with Node - The Enjoyable Way

Friends - I am here to tell you that there is indeed a better way. We owe this better way to the "node-fibers" and "wd-sync" libraries. This library isn't new - it's been around for a couple years now, actually. The inner workings of this library and the related concept of Node "generators" are complex and well outside the bounds of this post. If you'd like further information on that front, I recommend that you start here. For the purposes of this article though, allow me to show you what a Selenium test in Node using this approach looks like:

var link, url, textBox;

browser.init({  
    'browserName': 'chrome'
});

browser.get("http://admc.io/wd/test-pages/guinea-pig.html");  
assert(browser.title(), "WD Tests");

link = browser.elementById("i am a link");  
link.click();

url = browser.eval("window.location.href");  
assert(url.indexOf("guinea-pig2") >= 0);  
browser.back();

textBox = browser.elementByCss("#comments");  
textBox.type("Bonjour!");

assert(textBox.getValue() === "Bonjour!");

browser.quit();  

Long-story short: "node-fibers" allows us to write Selenium tests in a synchronous, "blocking" manner. Call a method (e.g. browser.get) and know that when the next line of code is executed, our previous request has returned the desired result. No asynchronous callbacks. No promise chains. Just pure and simple clarity.

For my part, I've written a simple library that builds on top of the foundation already provided by wd-sync. I suggest you check it out. It takes the guesswork out of writing Selenium tests on Node, creating custom actions ("plugins"), and generating test results.


Related Information: