How to Log All UI Test Method Calls Easily with ECMA6 Javascript Proxies
In the past, I’ve blogged about the UI tests that we run at Logz.io. We use the Intern framework, which wraps Selenium and actually runs the given browser. After every commit we make, our Jenkins build server produces the whole system, runs it in a Docker machine, then creates more Docker machines to run Selenium and do the UI-tests.
This process is crucial to giving us a safety net when deploying.
More on the subject:
However, this process has a lot of moving parts — so it’s prone to failures that aren’t always “real” (what we call “flaky tests.”) It’s also not the way I run the tests locally while writing them, so sometimes there are bugs that happen only in the Jenkins environment. These bugs are usually because of minor timing issues (that don’t represent “real” bugs) and are really hard to fix.
In the past, I would add endless console.log() calls until I would be able to pinpoint the problematic part. This process takes a tremendous amount of time. After each change, I need to commit and then wait for our Jenkins to build everything and finish running it to get the output.
Each web page in my UI tests is represented as a separate object, so I figured that I can create a simple proxy to wrap them and log each method call. This would help me to understand exactly where the test failed when looking at the log instead of going into the long and tiring debug loop that I described above.
Here’s a sample of what a web page object looks like in my test:
'use strict'; define(function (require) { let config = require('../config/config'); let MethodLogger = require('../utilities/method-logger'); function LoginPage(remote) { this.remote = remote; } LoginPage.prototype = MethodLogger({ constructor: LoginPage, navigateTo: function() { return this.remote.get(config.appUrl + '/login'); }, getUsername: function() { return this.remote .findByCssSelector('input.email') .getProperty('value'); }, enterUsername: function(username) { return this.remote .findByCssSelector('input.email') .click().type(username) .end(); }, // ... // ... }); return LoginPage; });
As you can see, the prototype of the LoginPage object is wrapped in a method called MethodLogger. This is my proxy, and this is how it looks:
define(function() { return function(target) { return new Proxy(target, { get: function(target, propKey, receiver) { var objName = target.constructor && target.constructor.name ? target.constructor.name : '[object]'; console.log(` --> ${objName}.${propKey}`); return Reflect.get(target, propKey, receiver); } }); }; });
And now, my test output looks like this:
Now, when there’s an error, it’s easier for me to see exactly where it happened.
Get started for free
Completely free for 14 days, no strings attached.