The Speed & The Enthusiasm

The Art of Protractor & Cucumber
Paul Li

This Meeting is Confidential.


No Photos or Recordings; No Online Posting.

Today’s presentation will also contain forward-looking statements concerning Yahoo!'s strategic and operational plans and expected performance.  Actual results may differ materially from those predicted due to risks and uncertainties, including the "Risk Factors" in Yahoo!'s annual and quarterly reports at www.sec.gov.

今日的簡報內容包含Yahoo!專有的機密資訊,這些資訊均受到您與Yahoo!之間的保密協議書(或含有保密協定的其他合約)之條款及條件所拘束。

About me

Paul Li
HTML / CSS / JavaScript / ActionScript

work @
  • Yahoo!

facebook:
http://www.facebook.com/mei.studio.li

Agenda

  • Protractor
  • Cucumber
  • Page Objects
  • dragonKeeper
  • impress
  • Q & A
  • References

Protractor

Protractor

Protractor is an end-to-end test framework for AngularJS applications. Protractor runs tests against your application running in a real browser, interacting with it as a user would.

  • Build in Node.js enviroment.
  • Promise based.
  • Testing almost everything which user can do.

Protractor - work flow

Protractor - API

Protractor

Protractor - Promise

itemPage.go().then(
    function(itemPage) {
        return itemPage.buyItemDirectly();
    }
).then(
    function(shoppingCart) {
        shoppingCart.clearOItem();
    }
).then(
    function() {
        itemPage = new PO(biddings.mid);
        return itemPage.go();
    }
).thenCatch(
    function(err) {
        //error occur
    }
).then(callBack);
            

Protractor

Protractor

Protractor

Protractor

Protractor

Protractor

However, there are still something non-testable.

Such as:

  • Analyze image content
  • Something not real time execute
  • Web Components

Protractor

Protractor - setup config files

var config = {
    seleniumAddress: 'http://localhost:4444/wd/hub',
    specs: [
        'cucumber/**/*.feature'
    ],
    suites: {
        booth: 'cucumber/booth/**/*.feature'
    },
    capabilities: {
        browserName: 'chrome'
    },
    framework: 'cucumber',
    cucumberOpts: {
        require: 'cucumber/**/*.js',
        tags: process.env.tags || '@BETA,@PP,@PROD',
        format: 'summary'
    }
};

config - seleniumAddress

setup test browser address, could be local or remote

seleniumAddress: 'http://localhost:4444/wd/hub'

config - specs

setup path of features, it could be array or string.

Array:

specs: [
    'cucumber/**/*.feature'
]

String:

specs: features/**/*.feature'

config - suits

setup suits for different purpose.

suites: {
    dragonMultiSpec: 'cucumber/dragon/dragonMultiSpec.feature',
    boothSRP: [
        'cucumber/booth/boothSRP.feature',
        'cucumber/booth/boothSRPImage.feature',
        'cucumber/booth/searchPreference.feature',
        'cucumber/booth/boothSRPImageText.feature'
    ]
}
protractor conf.js --suite boothSRP

config - capabilities

setup testing browser.

capabilities: {
    browserName: 'chrome'
}

or

capabilities: {
    browserName: 'chrome',
    shardTestFiles: true,
    maxInstances: 3
}

config - multiCapabilities

Yes! We can set multi-browser to run the same testings.

multiCapabilities: [
    {
        browserName: 'firefox'
    },
    {
        browserName: 'chrome'
    }
]

config - frameworks

framewok setup, here comes cucumber settings.

framework: 'cucumber',
cucumberOpts: {
    require: 'cucumber/**/*.js',
    tags: [
        process.env.tags || '@BETA,@PP,@PROD,
        '~@X'
    ],
    format: 'summary'
}

Start your engine

First things, open the command prompt and start the webdriver server:

webdriver-manager start

After that, you can run Protractor in another command prompt by typing:

protractor conf.js


Protractor

Cucumber

Cucumber is a tool for running automated tests written in plain language. Because they're written in plain language, they can be read by anyone on your team. Because they can be read by anyone, you can use them to help improve communication, collaboration and trust on your team.

  • Behavior Driven Development
  • Tag able
  • Separate steps describe from testing code
  • Steps reuseable

Cucumber - plain language

Start with "Given", "When", "Then" sentence to make step easy read.

  • Given: describe status done or fininsh something.
  • When: describe kind of action.
  • Then: expect some result.

Cucumber

Cucumber

Cucumber

Cucumber

Cucumber - feature

We can put many scenarios in one feature file. How to decide the scale depends on team's decision.

Feature: google index
    As a user of google
    I could do some search and I can get correct response.

    Scenario: 搜尋 "google", 第一筆資料顯示正確
        Given I visit "google"
        When I search "google"
        Then search result must have more than "1" record
        And first record title must be "Google"

Cucumber - step definition

We need to define each step its definition to make automation know how to do and what will be next.

//Given I visit "google"
Given(/I visit "([^"]*)"/, function(key, callback) {
    var url;

    switch (key) {
        case 'google':
            url = 'https://www.google.com.tw/';
            break;
    }//end switch

    browser.get(url).then(callback, callback);
});

Cucumber - step definition

//When I search "google"
When(/^I search "(.*)"$/, function(key, callback) {
    $('#lst-ib').clear().sendKeys(key).then(
        function() {
            $('#footcnt').then(
                function(e) {
                    e.click();
                },
                function(err) {
                    //err catch
                }
            )
        }
    ).then(
    

Cucumber

Cucumber - step definition

        function() {
            browser.getCurrentUrl().then(
                function(url) {
                    $('input[name="btnK"]').click().then(
                        function() {
                            browser.wait(
                                function() {
                                    return browser.getCurrentUrl().then(
                                        function(cUrl) {
                                            return cUrl != url;
                                        }
                                    );
                                }
                            , 5000);
                        }
                    );
                }
            );
        }
    

Cucumber - step definition

    ).then(
        function() {
            browser.wait(
                function() {
                    return $('#resultStats').isPresent().then(
                        function(flag) {
                            return flag;
                        }
                    );
                }
            , 5000);
        }
    ).then(callback, callback);
});

Cucumber - step definition

//Then search result should have more than "1" record
Then(/search result should have more than "(\d+)" record/, function(amount, callback) {
    var request = Number(amount);
    $('#resultStats').getInnerHtml().then(
        function(html) {
            html = html.replace(/,/g, '').replace(/約有 (\d+) 項結果.*/, '$1');
            return Number(html);
        },
        function() {
            return 0;
        }
    ).then(
        function(amount) {
            expect(amount, 'search result error').to.be.at.least(request);
        }
    ).then(callback, callback);
});

Cucumber - step definition

//Then first record title should be "Google"
Then(/first record title should be "(.*)"/, function(key, callback) {
    var request = key;

    $$('#ires li').get(0).$('a').getText().then(
        function(title) {
            return title;
        },
        function() {
            return '';
        }
    ).then(
        function(title) {
            expect(title, 'title is empty').to.not.be.empty;
            expect(title, 'title error').to.be.eq(request);
        }
    ).then(callback, callback);
});

Page Object<

Page Object

Page Object

Page Object

Page Object

var google;

google = function() {
    PageObject.call(this); // call super constructor.
    this.data.url = constants.URL_MAP.google;
    this.selector = {
        logo: '#hplogo',
        footer: '#footcnt',
        btnSearch: 'input[name="btnK"]',
        searchResultAmount: '#resultStats',
        results: '#ires li'
    };

    //components
    com_navigation = require(__base + constants.COM.navigation);
    this.navigation = new com_navigation();
};

Page Object

google.prototype = Object.create(PageObject.prototype);

google.prototype.goSearch = function(keyword) {
    ...
};

google.prototype.getSearchResultAmount = function() {
    ...
};

google.prototype.getFirstRecordTitle = function() {
    ...
};

module.exports = google;
            

Page Object

//Given I visit "google"
Given(/I visit "([^"]*)"/, function(type, callback) {
    var stand, param, type = type.replace(/\s/g, '');
    switch(true) {
        case (/^google/i.test(type)):
            stand = require(__base + constants.PO.google);
            break;
    }//end switch

    stand = new stand(param);
    this.stand = stand;

    stand.go().then(callback, callback);
});

Page Object

//When I search "google"
When(/^I search "([^"]*)"$/ function(key, callback) {
    this.stand.goSearch(key).then(
        function(google) {
            //do nothing
        }
    ).then(callback, callback);
});

Page Object

//Then search result must have more than "1" record
Then(/search result must have more than "(\d+)" record/, function(amount, callback) {
    var request = Number(amount);

    this.stand.getSearchResultAmount().then(
        function(amount) {
            expect(amount, 'search result error').to.be.at.least(request);
        }
    ).then(callback, callback);
});

Page Object

//And first record title must be "Google"
Then(/first record title must start with "(.*)"/, function(request, callback) {
    var request = new RegExp('^' + request + '\.*');

    this.stand.getFirstRecordTitle().then(
        function(title) {
            expect(title, 'title is empty').to.not.be.empty;
            expect(request.test(title), 'data match error').to.be.true;
        }
    ).then(callback, callback);
});

Live Demo

what else ?

Cucumber - event hook

var afterHooks = function() {
    this.registerHandler('AfterFeatures', function (event, callback) {
            // clean up!
            // Be careful, there is no World instance available on `this` here
            // because all scenarios are done and World instances are long gone.
            callback();   
    });
};
 
module.exports = afterHooks;

Cucumber - event hook

var stepResultHooks = function() {
    var fs = require('fs'), dir = __base + '/screenShots/';
    this.StepResult(function (event, callBack) {
        var stepResult = event.getPayloadItem('stepResult'), step = stepResult.getStep();
        if (stepResult.isFailed()) {
            browser.takeScreenshot().then(function (png) {
                var stream, fname;
                fname = 'err_' + new Date().getTime() + '_' + step.getLine() + '_' + step.getName() + '_' + new Date().toISOString() + '.png';
                fname = fname.replace(/"|'|\//g, '').replace(/\s|:|>/g, '_');
                stream = fs.createWriteStream(dir + fname);
                stream.write(new Buffer(png, 'base64'));
                stream.end();
            }).then(callBack);
        } else callBack();
    });
};

step error

step error

step error

step error

step error

dragonKeeper

dragonKeeper

We have already code so many scenarios in different features. 「What if we can rearrange these scenarios order, we might test the whole flow ??」
This is a really good idea for team and me.

This is the main purpose drive me to create dragonKeeper.

  • Prepare your eggs.
  • Execute dragonKeeper.js
  • dragons come up

Prepare your egg

execute dragonKeeper

Dragons come up

Live Demo

impress

impress

It's kind of diffcult to write a requirement document for all targets. Even you've already wrote one, it might lost its validity when time pass.

targets:

  • Boss
  • Web developer
  • Quality engineer

Step1 - prepare features

Step2 - Execute impress.js

node impress.js

impress - before

impress - after

References

References

Q and A

Thank you