Protractor, Jasmine and promises for non-browser functions

Since end of October I switched from test automation using Groovy, Geb, Spock and SoapUI to test automation using Protractor, Cucumber and Jasmine. Different apps, different tools, new challenges and new knowledge.

The biggest challenge is promises. Lots of samples on the web about promise chaining. The small "problem" - most of those samples are for browser interaction related functions. Well, that's what we are dealing most in e2e tests. But what if those simple functions are not enough, if you need to write some functions of your own which return non-browser related promises? E.g., just to compare some values of complex structure and return "true" or "false"? Or anything else not browser related.

If you are new to this kind of testing, here's where "adventures" begin. When you don't know what to google for, it's not so easy to find out you are looking for a thing named "browser control flow". What it is about? Well, here I'm going to quote post by Ben Heymink:
Behind the sciences, WebDriver, the underlying component driving your e2e tests, maintains this queue of promises, called the ‘Control Flow’ in order to keep everything executing in the correct order. Protractor actually modifies Jasmine so that each test spec waits until this ‘control flow’ queue is empty before exiting.
Jasmine expectations are also adapted to understand these promises.
What does it mean? Simple -- your "custom" function calls must be added to this control flow if you want them to be executed in the same queue with WebDriver promises. You need to add just a small piece of code and everything starts working as expected:

let flow = browser.controlFlow();
flow.execute(() => {
   //your code here
});

Actually, this is a perfect way to debug code while working (I like command line debugging).

Here's a simple example which prints everything in the expected order. Just try removing all or several flow.execute calls and notice how results change.

describe("Promise handling test", function () {
    it("shows how to synchronize custom promises", function () {
        let flow = browser.controlFlow();
        flow.execute(() => {
            console.log("before all tests");
        });
        flow.execute(sayHelloAsync1)
            .then((res) => {
                console.log(res);
            });
        flow.execute(sayHelloAsync2)
            .then((res) => {
                console.log(res);
            });
        flow.execute(sayHelloAsync3)
            .then((res) => {
                console.log(res);
            });
        flow.execute(() => {
            console.log("after all tests");
        });
    }, 60 * 1000);
});

function sayHelloAsync1() {
    let deferred = protractor.promise.defer();
    setTimeout(function () {
        return deferred.fulfill('Hello World1');//here we resolve the promise
    }, 5000);
    return deferred.promise;
}

function sayHelloAsync2() {
    let deferred = protractor.promise.defer();
    setTimeout(function () {
        return deferred.fulfill('Hello World2');//here we resolve the promise
    }, 3000);
    return deferred.promise;
}

function sayHelloAsync3() {
    let deferred = protractor.promise.defer();
    setTimeout(function () {
        return deferred.fulfill('Hello World3');//here we resolve the promise
    }, 1000);
    return deferred.promise;
}

Current result is :


If I change call to sayHelloAsync1 and printing output as I would do with "normal" browser promises:
sayHelloAsync1()
    .then((res) => {
        console.log(res);
    });

then the result is a bit unexpected: it might print or might not. Depends what happens first: browser control flow queue is cleared or this promise resolved. With current timeout (5000) it should not be printed. If I change 5000 to 1000 -- it has more chances to be printed, as other two promises in browser control flow take longer to execute (3000 + 1000). In real life you never know how long it will take for promise to be resolved. So though most of the time you might think "everything works", one day your test suddenly might fail. Just because some promises were resolved quicker / took to resolve longer than usual.

And to make life even more interesting -- this is going to change in ECMAScript2017 and to be replaced with await. See GitHub and MDN for details.

Comments

Popular posts from this blog

Migrate Protractor E2E tests to async / await

Allure reporter, Gradle and JUnit5