Does Protractor + Selenium WebDriver Sound "Promising?"

I have been diving into JavaScript and AngularJS heavily over the past few weeks, pushing the capabilities of my organization's application testing in the process.  Despite having written quite a bit of JavaScript in my past for a number of award-winning Web applications (in hackathons), some of the latest trends in that language had bypassed me completely.  In coming up to speed on JavaScript Promises, here is some code that has proven very useful in my activities.

Typically, doing Web page testing requires interacting with the user interface, then waiting for something to happen (you logged in, paid your bill, ordered food, wrote a review, etc.).  There are three ways to wait for such UI interactions to complete in JavaScript:

  • Pure asynchronous callbacks (nested and ugly)
  • Unchained promises (still can be nested, and has potential to get ugly)
  • Chained promises (Pretty straightforward)
The essence of these three methodologies is described succinctly and effectively on the GitHub page for WD.js (a popular WebDriver for Selenium automated GUI tests).  My organization has elected to use Protractor as its framework of choice for our AngularJS tests, plus Gulp as our build manager, and this tends to force the use of "selenium-webdriver" as opposed to WD.js.  The constructs in WD.js are mostly dissimilar and require conversion in order to be compatible with selenium-webdriver (perhaps an open-source hero could be made if someone wrote a converter!).  For instance, selenium-webdriver only seems to be happy with unchained promises, where WD.js seems to accept all types of promises.  Thus, it is important to note the following code pertains to Protractor and selenium-webdriver.

Waiting On External Events


In some cases, you may not wish to wait for changes to a UI element on the browser, but instead for a connection and query to a database or perhaps a call to an external API.  For this, most people use the "Q" Node module; however, selenium-webdriver already includes the "Q" framework through browser.controlFlow().  Specifically, browser.controlFlow().execute(<some function defined as a variable>).  The function you name must return a promise.

Here's a quick example, including a hypothetical function to connect to a database, and the test code that would wait for this function to return the connection object:

connectToDB = function() {
    var promise = protractor.promise.defer();
    var mydb = require('dbhelper');
    mydb.getConnection({
        // some hard-coded settings
    }, function(err, connection) {
        if (err)
            promise.reject(err.message);
        else
            promise.fulfill(connection);
    }
    return promise.promise;
};

describe('a test suite', function() {
    it('should connect to the DB', function() {
        browser.controlFlow().execute(connectToDB).then(function(conn) {
            console.log('I now have a connection!');
        });
    });
});

Side Note: Yes, you can use JavaScript to connect to databases directly, even Oracle databases!  Just check out the recent (and official) node-oracle Node module if you need connectivity to Oracle DBs.  I'm sure there are other great ones for MySQL, Microsoft SQL Server, MariaDB, Cassandra, etc.

This is all fine and dandy, but note that connectToDB is not set up in such a way as to take arguments.  First, the function needs to return promise.promise to execute() rather than the connection object that connectToDB() makes.  Second, due to the way control flows work, nothing is really supposed to get run right when tasks are defined, thus the parentheses must be omitted from connectToDB when it is passed as an argument to execute(), because otherwise it will get run right away.  Only when the entire test flow described inside it() has been parsed may functions returning promises get run.

This makes it rather difficult to pass arguments to functions that return promises!  Does this mean you'll have to handle making your database query in the same function, and lose the capability to abstract that into a generic query function?  No!  And, in fact, it's not difficult at all to pass arguments to such functions.

Passing Arguments to Promise Functions


Given a function select(conn, query) that runs a select-type query specified by query on the database connection conn, you can pass those arguments and still wait for the promise as such:

// code from before, plus definition of "query"...
browser.controlFlow().execute(connectToDB).then(function(conn) {
    // simply elaborating on the contents of this same function from above
    var qr = function() {    // qr = query request ;)
        return select(conn, query);
    }
    browser.controlFlow().execute(qr).then(function(rows) {
        console.log(rows);
    });
});

Voila, a control flow that can wait on a database connection and query!


Help!  I can't see my code anymore!


The astute observer may have noticed that with enough of these .then() calls, your code will be so far to the right that you will constantly have to scroll in order to see it.  In this sense, it's no better than using callbacks.  However, other mechanisms in Protractor allow you to remedy this.  You can run promises all at once, and then wait for them all to complete, by using this construct:

var ar = function() {    // hypothetical API request
    httpRequest(settings);    // settings = what you use for http.request(settings, ...) where http = require('http')
}
var ar2 = function() {    // hpothetical request #2
    httpRequest(settings2);
}
protractor.promise.all([browser.controlFlow().execute(ar), browser.controlFlow().execute(ar2)].then(function(responseArr) {
    // Assuming httpRequest() fulfills the promise with the raw HTTP response:
    console.log(responseArr[0]);    // response from ar
    console.log(responseArr[1]);    // response from ar2
});

Just use protractor.promise.all([<array>]).then, and you can greatly cut down on indentations required.  This even works, of course, on native selenium-webdriver functions that don't require being wrapped in execute().  Unfortunately, when making HTTP calls, my experiments showed that these calls were not run in parallel.  Then again, the calls I made returned very quickly; perhaps if I made a request involving more latency, I might see more parallel behavior.

A Word Of Caution Regarding Loops


If you have code inside loops that calls other functions based on the loop iterator's value, it's easy to run into a situation where the function uses the final value of the loop iterator each (or most of the) time it runs.  For instance, if inside the second .then() function from the database example above, you wanted to call external APIs based on data in each row retrieved, you could end up executing that API call using the last row as the parameter each time unless you heed some basic JavaScript syntax advice.

browser.controlFlow().execute(qr).then(function(rows) {
    for (r in rows) {
        (function(row) {
            // Make the HTTP request based on each row in here
        })(rows[r]);
    }
});

This essentially creates a whole bunch of function instances where row is defined within the scope of each instance, not globally; then these functions get executed in series using the data you expect.  I bet that by assigning each function as an array element, you could possibly kick off all these functions in parallel with some other clever constructs!

Comments

Popular posts from this blog

Less Coding, More Prompt Engineering!

Start Azure Pipeline from another pipeline with ADO CLI & PowerShell

An Augmented Reality Experience to Complement a Vintage Pinball Machine