Introduction

OPA5 is a testing framework for SAPUI5 and OpenUI5 applications and components and is based on the open source testing frame work QUnit which is developed by the JQuery developers.

OPA5 is shipped with a build in mock server based on sinon.js. The only supported transport format is OData. The built in mock server cannot be used in case the back end is using JSON bodies.
Instead of using the mock server its foundation sinon.js is required and the individual routes are mocked manually.

The majority of the tests can be described as integration tests without a backend service.

Instead of running in an IFrame, the newer component functions of OPA5 will be used for testing. This will allow a faster execution and better debugging in case of errors.

Links

Test Structure

Files and Folders

The structure will allow an execution of individual feature tests. It looks like:

- test
  - arrangements
    - Common.js
  - integration
    - pages
      - app
        - App.js
      - ...
        - ...
    - App.js
    - ...
  - sinon
    - routes
      - App.js
      - ...
    - Utils.js
    - sinonServer.js
  - AllOPATests.js
  - configureAndRequire.js
  - index.html
- webapp
  - controller
  - i18n
  - view
  - index.html
  - Component.js

File Loading

The test/index.html loads the UI5 libraries, which are required for running the tests. Once the DOM is ready test/AllOPATests.js will be loaded synchronously. It will require test/configureAndRequire.js synchronously. test/configureAndRequire.js will define global constants and require modules and components which need to be loaded synchronously. Afterwards test/AllOPATests.js will load the tests and mocked routes asynchronously.
Such a setup allows the dedicated execution of test modules without repeating code by creating additional html and test/*.js files. This can be useful for parallel execution of tests when the UI5 app becomes complex and the test execution time slows down the build.

Code Abstraction

Abstracted tests, which can be used with parameters at any module are placed at test/integration/pages/CommonFunctions.js. The individual page modules and test definitions are placed at test/integration/pages/moduleGroup/moduleName.js. Each of this individual page modules will require the test/integration/pages/CommonFunctions.js by defining the baseClass property:

The modules and tests are placed at test/integration/. In any test any of the individual page objects can be used (Given, When, Then).

Manual Test Execution

The individual tests are structured in modules using the Qunit.module function. This allows to execute a specific module by GET parameter.

  • All Tests at once https://domain.tld/test/index.html
  • Specific module https://domain.tld/test/index.html?module=App

Backend Mocking

The biggest challenge running UI tests is the execution time. A full test coverage done as integration tests with Selenium will most likely lead to long test runs. This will also happen if the tests are run headless with PhantomJS.

As a consequence the backend calls need to be mocked. sinon.js is great for this and already shipped with UI5.

Creating Routes

All mocked backend services are placed at test/sinon/routes. Each menu group has its own file. Any request which is not directed to the api path /api/v1/ will not be mocked. In case the request is to this target, but the target is not mocked yet, a 404 will be returned by sinon.js.

The individual routes are registered to the global object oServer, which is created within the file mock/sinonServer.js.

A flexible way with regular expression could be:

oServer.respondWith("HEAD", /\/api\/v1\/(?:.+)\/(?:.+)/, [200, {
    "Content-Type": "application/json"
}, ""]);

Precise GET route:

oServer.respondWith("GET", "/api/v1/users/1", [200, {
    "Content-Type": "application/json"
}, JSON.stringify([{
    "userName": "Jon Doe",
    "email": "jon@doe.tld"
}])]);

Registering Mock Files

The created routes will not be active, unless they are required before the test is executed. This should be done within the file loading the tests like AllOPATests.js.

Tests

The test structure is complex, including global and local arrangements, actions and assertions. Their definition is done outside of the scope of each test. Still it will allow the tests to be executed in an atomic way.

Integration Test Structure

All tests should be grouped into modules. It is recommended to create one module per view. Each module has its own file. The file which will require the sinon.js model is placed at test/integration/moduleName.js.

Each test should be as atomic as possible. The required tear down of the component is already configured in the afterEach callback within the global oModuleConfig.

The structure and keywords is similar to Cucumber and includes Given for arrangements, When for actions and Then for assertions.

Defining a Module

After the needed files are required, the module name is declared. While declaring the module, the global config oModuleConfig with afterEach and beforeEach is applied. The selected name should be unique as it is used for isolated test runs.

Arrangements

Arrangements are started with the Given keyword. The arrangements of each test should define the view or better its URL hash, which shall be tested.
Make sure to check if the component and application is loaded, before doing any actions.

Actions

Actions are executed with the When keyword. They are used to simulate/indicate a user behavior like a click on a link. Or the press/tab on a button or icon. No matter which module is tested, the actions of any page object can be used by specifying the page object and action name in dot notation like When.fillInMyPageObjectName.doSomeAction(“Parameter1”, “Parameter2”);.

Assertions

Assertions are done with the Then keyword. An assertion checks if the actual outcome of an arrangement and optional action(s) is as expected. Each test can have N assertions. Like actions, the assertion of any page object can be used in any module.

Example

QUnit.module("Module Name", oModuleConfig);

opaTest("My Atomic test", function(Given, When, Then) {

    // Arrangements
    Given.iStartMyApp("Users");
    Given.componentLoaded();
    Given.applicationDataLoaded();

    // Actions
    When.fillInMyPageObjectName.doSomeAction("Parameter1", "Parameter2");

    // Assertions
    Then.pageObjectNameUnderTest.assertionFunctionOne("Parameter1");
    Then.pageObjectNameUnderTest.assertionFunctionTwo();
});

Shared Test Functions

There are three levels of abstraction to arrangements, actions and assertions.

  • An abstract utility class can be defined using sap.ui.define and at a later point included by either sap.ui.define or sap.ui.require
  • Functions which are only slightly different and used in more than one page object are placed into test/integration/pages/CommonFunctions.js and loaded by sap.ui.define; Each page object is then referencing it with the baseClass property
  • Functions which are used only at a specific page object are placed only within the specific object

this.waitFor

Nearly any test will use the function waitFor. It belongs to the Opa5 control and allows to wait for a given state, before doing an assertion or action. It is always good to define the view, where the element is placed in. If the id of the element is known, specify it as well.

In case a control has no id, specify the type of the control like controlType: “sap.tnt.NavigationListItem”. If combined with a matcher, specific list items, buttons and, etc. can be access like:

{
  matchers: new sap.ui.test.matchers.PropertyStrictEquals({
    name: "text",
    value: "To be found value"
  }),
}

Sometimes custom checks might be required. An example would be a message toast:

matchers: function() {

    // Workaround with remove to increase speed of testing
    var oMessageToast = jQuery(".sapMMessageToast");
    var sText = oMessageToast.text();

    if (oMessageToast) {
        oMessageToast.remove();
    }

    return sText;
}

Use success, in case it is sufficient if the element exists. The assertion will be executed even if the element is not fully animated, rendered or invisible.

In case you need to wait for an animation or the a general visibility to the user (like with dialogs or links) use the action property, to define the assertions.

An example for a test, which uses waitFor could be:

/**
 * Test if an element has any text and visibility is true.
 * @param {string} sViewName - ID of the view.
 * @param {string} sId - ID of the to be checked element.
 * @param {string} sSuccessText - Success message.
 * @returns {object}
 */
iShouldSeeText: function(sViewName, sId, sSuccessText) {
    return this.waitFor({
        id: sId,
        viewName: sViewName,

        /**
         * Executed when the object is found, visible and animated.
         * @param {object} oElement - Found object.
         * @returns {void}
         */
        success: function(oElement) {
            ok((oElement.getText().length > 0), sSuccessText);
        },
        errorMessage: "Could not find text at view " + sViewName + " and id " + sId
    });
},

 

CommonFunctions.js

The file test/integration/pages/CommonFunctions.js contains tests, which can be easily abstracted. Examples could be:

  • control exists
  • control has any text
  • control is visible
  • control has a given text
  • control has a given amount of list items

Common.js

test/arrangements/Common.js contains the common app arrangements. The the app is started as a component to have better speed and debugging possibilities. In addition the arrangement checks if the component could be loaded and if the application could be started are defined.

ModuleName.js

Each page object file is started by sap.ui.define which will load Opa5, CommonFunctions.js and the mocked route(s). The majority of the test actions and assertions will probably be calls to the functions from CommonFunctions.js. Only a minority is page specific and defined within the page object itself.

A skeleton template looks like:

sap.ui.define([
    "sap/ui/test/Opa5",
    "test/integration/pages/CommonFunctions"
], function(Opa5, CommonFunctions) {

    "use strict";

    Opa5.createPageObjects({
        onTheApp: {
            baseClass: CommonFunctions,
            actions: {
            },
            assertions: new Opa5({
            })
        }
    });
});

 

Example Files

test/AllOPATests.js

/* global checkStart */
jQuery.sap.require("test.configureAndRequire");

// The first require block loads the definitions like shared classes, assertions and so on
sap.ui.require([
    "sap/ui/test/Opa5",
    "test/arrangements/Common",
    "test/integration/pages/app/App",
    "test/sinon/routes/App"
], function(Opa5, Common) {

    "use strict";

    // Define the context of the to be tested application including the base view name
    Opa5.extendConfig({
        viewNamespace: "your.app.view.",
        viewName: "App",
        autowait: true,
        arrangements: new Common()
    });

    // Require the individual tests and mocked routes
    sap.ui.require([
        "test/integration/App"
    ], function() {

        // Only when all files are required, start the Qunit tests.
        checkStart();
    });
});

test/configureAndRequire.js

/* global QUnit, jQuery, sinon */

var oModuleConfig;// eslint-disable-line no-unused-vars

// Require the testing framework
jQuery.sap.require("sap.ui.qunit.qunit");
jQuery.sap.require("sap.ui.qunit.qunit-css");
jQuery.sap.require("sap.ui.qunit.qunit-junit");
jQuery.sap.require("sap.ui.thirdparty.qunit");

//KarmaJS might have loaded SinonJS
if (typeof sinon === "undefined") {
    jQuery.sap.require("sap.ui.thirdparty.sinon");
    jQuery.sap.require("sap.ui.thirdparty.sinon-server");
}

jQuery.sap.require("test.sinon.sinonServer");

/**
 * Delayed start of QUnit or KarmaJS might break.
 * @returns {void}
 */
function checkStart() {// eslint-disable-line no-unused-vars

    "use strict";

    var aModules, i, iLength;

    if (!window["sap-ui-config"] || !window["sap-ui-config"].libs || !sap) {

        setTimeout(checkStart, 500);

        return;
    }

    aModules = window["sap-ui-config"].libs.replace(/sap./g, "").replace(/\s/g, "").split(",");

    for (i = 0, iLength = aModules.length; i < iLength; i++) {

        if ((aModules[i].indexOf(".") !== -1 && !sap[aModules[i].split(".")[0]]) || (aModules[i].indexOf(".") === -1 && !sap[aModules[i]])) {

            setTimeout(checkStart, 500);

            return;
        }
    }

    QUnit.load();
    QUnit.start();
}

/*
 * This object will be used whenever a module is defined.
 */
oModuleConfig = {

    /**
     * Executed before each test.
     * @returns {void}
     */
    beforeEach: function() {

        "use strict";
    },

    /**
     * Executed after each test.
     * @returns {void}
     */
    afterEach: function() {// eslint-disable-line require-jsdoc/require-jsdoc

        "use strict";

        // Hacks to ensure the component is teared down even in case an element could not be found
        var clock = sinon.useFakeTimers();

        new sap.ui.test.Opa5().iTeardownMyUIComponent();
        sap.ui.test.Opa.emptyQueue();
        clock.tick(1000);
        clock.restore();
    }
};

// Do not auto start, to ensure all components are loaded
QUnit.config.autostart = false;

test/index.html

<!DOCTYPE HTML>
<html>
<head>
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <title>OPA5 Tests</title>

   <script
       id="sap-ui-bootstrap"
       src="/resources/sap-ui-core.js"
       data-sap-ui-libs="sap.m"
       data-sap-ui-theme="sap_belize"
       data-sap-ui-language="en"
       data-sap-ui-compatVersion="edge"
       data-sap-ui-preload="async"
       data-sap-ui-frameOptions="deny"
       data-sap-ui-animation="false"
       data-sap-ui-resourceroots='{ "test": "/test", "your.app": "/webapp/" }'>
   </script>
</head>
<body>
   <div id="qunit-wrapper">
      <div id="qunit"></div>
      <div id="qunit-fixture"></div>
   </div>
   <script type="text/javascript">jQuery.sap.require("test.AllOPATests");</script>
</body>
</html>

test/arrangements/Common.js

/* global ok */

sap.ui.define([
    "sap/ui/core/routing/HashChanger",
    "sap/ui/test/Opa5",
    "sap/ui/test/matchers/PropertyStrictEquals"
], function(HashChanger, Opa5, PropertyStrictEquals) {

    "use strict";

    var Common = Opa5.extend("test.arrangements.Common", {

        /**
         * Start the app via Component.js for best performance and easier debugging.
         * @param {string} sFunctionHash - Manipulate URL to this function name before starting the component.
         * @returns {object}
         */
        iStartMyApp: function(sFunctionHash) {

            var sNewHash = String(sFunctionHash || "");

            if (jQuery(".sapUiOpaComponent").length !== 0) {
                this.iTeardownMyUIComponent();
            }

            HashChanger.getInstance().replaceHash(sNewHash);

            return this.iStartMyUIComponent({
                componentConfig: {
                    name: "sap.hcp.analytics.mco"
                },
                hash: sNewHash
            });
        },

        /**
         * Arrangement if the component could be loaded.
         * @returns {object}
         */
        componentLoaded: function() {
            return this.waitFor({
                viewName: "App",

                /**
                 * Check if jQuery can find a div with the message toast class.
                 * @returns {boolean}
                 */
                check: function() {
                    return (jQuery(".sapUiOpaComponent").length !== 0);
                },

                /**
                 * Success callback. Element might be hidden.
                 * @returns {void}
                 */
                success: function() {
                    ok(true, "The component was loaded");
                },

                /**
                 * Error callback.
                 * @returns {void}
                 */
                error: function() {},
                errorMessage: "Could not load component"
            });
        }
    });

    return Common;
});

test/integration/App.js

/* global QUnit, oModuleConfig */

sap.ui.require([
    "sap/ui/test/Opa5",
    "sap/ui/test/opaQunit"
], function(Opa5, opaTest) {

    "use strict";

    QUnit.module("test/integration/App.js", oModuleConfig);

    opaTest("Menu has expected items", function(Given, When, Then) {

        // Arrangements
        Given.iStartMyApp("404");
        Given.componentLoaded();

        // Assertions
        Then.onTheApp.theMenuHasItems().iTeardownMyUIComponent();
    });
});

test/integration/pages/CommonFunctions.js

/* global ok, strictEqual */

sap.ui.define([
    "sap/ui/test/Opa5",
    "sap/ui/test/matchers/Properties",
    "sap/ui/test/matchers/PropertyStrictEquals",
    "sap/ui/test/actions/Press"
], function(Opa5, Properties, PropertyStrictEquals, Press) {

    "use strict";

    var CommonFunctions = Opa5.extend("test.integration.pages.CommonFunctions", {

        /**
         * Test if an element has any text.
         * @param {string} sViewName - ID of the view.
         * @param {string} sId - ID of the to be checked element.
         * @param {string} sSuccessText - Success message.
         * @returns {object}
         */
        iShouldSeeSomeText: function(sViewName, sId, sSuccessText) {
            return this.waitFor({
                id: sId,
                viewName: sViewName,

                /**
                 * Executed when the object is found, visible and animated.
                 * @param {object} oElement - Found object.
                 * @returns {void}
                 */
                actions: function(oElement) {
                    ok(oElement.getText(), sSuccessText);
                },
                errorMessage: "Could not find any text at view " + sViewName + " and id " + sId
            });
        }
    });

    return CommonFunctions;
});

test/integration/pages/app/App.js

/* global strictEqual, ok */

sap.ui.define([
    "sap/ui/test/Opa5",
    "test/integration/pages/CommonFunctions"
], function(Opa5, CommonFunctions) {

    "use strict";

    Opa5.createPageObjects({
        onTheApp: {
            baseClass: CommonFunctions,
            actions: {
            },
            assertions: new Opa5({

                /**
                 * Check if the site menu has a given list of items.
                 * @param {string} sViewName - Name of the to be loaded.
                 * @returns {object}
                 */
                theMenuHasItems: function(sViewName) {
                    return this.waitFor({
                        id: "sideNavigationList",
                        viewName: "App",

                        /**
                         * Executed when the success check becomes true.
                         * @param {object} oElement - Found object.
                         * @returns {void}
                         */
                        actions: function(oElement) {

                            var aItems = oElement.getItems();

                            strictEqual(jQuery.sap.equal([{
                                "title": "Home",
                                "icon": "sap-icon://home",
                                "key": "Home"
                            }], [{
                                "title": aItems[0].getText(),
                                "icon": aItems[0].getIcon(),
                                "key": aItems[0].getKey()
                            }]), true, "The menu contained expected items");
                        },
                        errorMessage: "The menu did not contain expected items"
                    });
                }
            })
        }
    });
});

test/integration/sinon/sinonServer.js

/* global sinon */

// Start the sinon.js fake server
var oServer = sinon.fakeServer.create();

oServer.autoRespond = true;

// Only those requests, which are directed to the SWA models, should be mocked.
oServer.xhr.useFilters = true;
oServer.xhr.addFilter(function(sMethod, sUrl) {

    "use strict";

    // whenever the regex returns true the request will not faked
    return (!sUrl.match(/\/api\/v1\//));
});

test/integration/sinon/routes/App.js

/* global oServer */

sap.ui.require([], function() {

    "use strict";

    oServer.respondWith("HEAD", /\/api\/v1\/(?:.+)\/(?:.+)/, [200, {
        "Content-Type": "application/json"
    }, ""]);
});
To report this post you need to login first.

1 Comment

You must be Logged on to comment or reply to a post.

  1. Shashikant Kambhattla

    A very useful article. I’m also using OPA5 to write integration tests for my application. But I’m stuck with this error:

    1. Uncaught Error: OpaFrame error message: Uncaught Error: Fake server request processing threw exception: INVALID_STATE_ERR – 4,
    2. url: https://webidetesting1099503-fiori.dispatcher.cert.hana.ondemand.com/src/test/resources/sap/ui/thirdparty/sinon.js?eval, line: 184
      Source:
      https://webidetesting1099503-fiori.dispatcher.cert.hana.ondemand.com/src/test/resources/sap/ui/test/launchers/iFrameLauncher.js?eval:6

    How to solve this problem?

    (0) 

Leave a Reply