4/06/2015

Look! No Hands! - Using Selenium and Node.js for interactive UI testing

Selenium is the most talked about UI testing framework that I could find that was both open source and well supported. We decided to adopt it for our automated UI testing. I have found it to be very useful, albeit with a stiff learning curve. One downside for me is the back and forth between building tests and then waiting for Selenium and the browsers to open and run the tests. It occurred to me I could help alleviate this by having the browsers at the ready and eagerly waiting for the command to start testing. So I reached into my toolbox to grab one of my favorite tools, Node.js. Using Node.js I was able to build an interactive interface with which I can iteratively build my front end tests. Everything I needed was already there, I just had to put pieces together.

I created a simple node server that can control the selenium process for me. I type a command into the console of my running Node server, and it instructs the already open browsers to run the tests. I like this approach because I can build my tests a little bit at a time and confirm they are working as I go. This is all possible because of the web driver component, which includes a JavaScript API, created by the Selenium team. The API instructs the running Selenium server to sends commands remotely to the browser being tested.

We start our node server with these required modules:

  • selenium-webdriver: The base library for the web driver which is used to build the driver objects
  • selenium-webdriver/remote: The module that starts up the Selenium Server
  • child_process: Each test is going to run in its own thread so the main thread isn't closed by the web driver when the tests complete

This is the require block:

var webdriver = require('selenium-webdriver');
var remoteServer = require('selenium-webdriver/remote').SeleniumServer;
var child = require('child_process').fork;

When Selenium starts a browser session it assigns a unique id for internal tracking. We are going to need that id. The id will let us attach our tests to the open browsers each time we want to run them. To that end, we are going to load the browser session id into a variable. This could also be done with an array or object instead of a variable when multiple browsers are being used.

var chromeId;

Everything we need is loaded so it is time to start the Selenium server. You will need to download the selenium server standalone jar file. Starting the server requires pointing to this jar file. The Selenium web driver uses promises so we are going to chain some of our commands together.

var server = new remoteServer('selenium/selenium-server-standalone-2.45.0.jar', {
    port: 4444
})
.start()
.then(function(){

The Selenium server should now be started and running. You can confirm this by going to the Selenium server through a web browser. The selenium server publishes a list of all sessions it is currently hosting to a web page on the server. The url will be something similar to "http://localhost:4444/wd/hub". Currently, the session list is empty but the page should come up.

Now that the server is started, it's time to tell it to load our testing browser. This is accomplished through a series of configuration functions that ends with build. Build is synchronous and will return the working web driving object for the new browser window.

var chromeDriver = new webdriver.Builder()
    .usingServer('http://localhost:4444')
    .withCapabilities(webdriver.Capabilities.chrome())
    .build();

We need to get Selenium's id for the new browser window before we can run any tests. Each web driver will have a session object once built and after the driver navigates to it's starting point. The first call will be a "get" to tell the server to navigate to a starting url. This is asynchronous and promise based, so we will add a function to the "then" method for the promise. Inside, the first promise resolution we can ask Selenium to get Selenium's session for this browser. This is also asynchronous and promise based so we will make one more resolve function inside the the get session resolution. This second resolve will be passed a session object from which we grab the id property and we pass that to our id variable created earlier.

chromeDriver.get("http://mytestapp")
.then(function (){ 
    chromeDriver.getSession()
    .then(function (s){
            chromeId = s.id_;
            chromeDriver.manage().timeouts().setScriptTimeout(0);
    })
});

Now we have everything we need to run our tests. The final step is to tell our node server to respond to the start command. A listener is attached to the node processes stdio pipe for a "readable" event. The "readable" is fired after the user hits the enter key. Our start command for this sample is the number 1. The process input and output pipes could be greatly expanded to be a menu based system or to include user instructions. We are keeping it simple for our purposes today. When the start command is read, node will launch a new fork and give that fork the id gleaned earlier. The fork will call a second JavaScript file with our actual tests. The second Javascript file is going to pull the session id from it's environment, and will ask the running Selenium server to attach to that running browser. Since Selenium can match that id to an existing session we will be able to run our tests on the already open window.

process.stdin.on('readable', function (){
    var val = process.stdin.read();
    if (val == 1) {
        child('testLauncher.js', [], {
           env:{
               session: chromeId,
           }
        });
    }
});

A new fork is started every time so the files the fork touches are read fresh every time. We can continue to grow our tests and then run them without fear of a cached version being used. As we improve our tests, we return to our node prompt, enter 1 and then watch Selenium run it's magic right before our eyes.

Below is the complete server code:

var webdriver = require('selenium-webdriver');
var remoteServer = require('selenium-webdriver/remote').SeleniumServer;
var child = require('child_process').fork;

var chromeId;

var server = new remoteServer('selenium/selenium-server-standalone-2.45.0.jar', {
    port: 4444
})
.start()
.then(function(){
    var chromeDriver = new webdriver.Builder()
        .usingServer('http://localhost:4444')
        .withCapabilities(webdriver.Capabilities.chrome())
        .build();

    chromeDriver.get("http://mytestapp")
    .then(function (){ 
        chromeDriver.getSession().then(function (s){
            chromeId = s.id_;
        })
    });

    process.stdin.on('readable', function (){
        var val = process.stdin.read();
        if (val == 1) {
            child('testLauncher.js', [], {
               env:{
                   session: chromeId,
               }
            });
        }
    });
});