Headless OPA5 Testing of Fiori Launchpad Apps with Code Coverage and Karma
Executive summary
OPA5 integration testing of SAP Fiori launchpad apps, including code coverage, isn’t supported by sap.ui.test.Opa5. This blog post provides a solution.
Automation of OPA5 integration testing of SAP Fiori launchpad apps with code coverage is also presented.
Author and motivation
Laszlo Kajan is a full stack Fiori/SAPUI5 expert, present on the SAPUI5 field since 2015.
The motivation behind this blog post is to provide an automated integration testing solution for Fiori launchpad apps – a feature so far missing from sap.ui.test.Opa5.
Headless OPA5 testing of Fiori launchpad apps with code coverage and Karma
Goal
- Implement automated OPA5 integration tests for SAPUI5 launchpad apps
- Provide code coverage results
- Employ current OPA5 best practices
App folder structure
- webapp
- test
- integration
- arrangement
- component
- Arrangement.js
- component
- pages
- Common.js
- AllJourneys.js
- opaTestsWithComponent.qunit.html
- arrangement
- karma
- context.html
- launchers
- ushellLauncher.js
- integration
- fakeLRep.json
- test.html
- test
- karma.conf.js
- package.json
Issue 1: Missing ‘iStartMyUIComponentInUshell’
The SAP WebIDE runs Fiori launchpad apps with mock data by running the app’s Component.js in a sap.ushell.Container. OPA5 testing allows apps to be started in an iframe – iStartMyAppInAFrame, or by their Component.js – iStartMyUIComponent. As of 20180809, test code coverage analysis only works in case the app is not started in an iframe. When iStartMyUIComponent is used to start the app, unlike starting it with mock data, Component.js is not placed into a sap.ushell.Container. Because of this, application code that otherwise works well – e.g. access to launchpad services – fails, leading to test errors. This is the first issue to solve.
How to run OPA5 integration tests with code coverage results, in a way similar to how the app is run with mock data? There is no OPA5 method ‘iStartMyUIComponentInUshell’ that would start a Component.js in a sap.ushell.Container. Let us define one:
- webapp/test/integration/arrangement/component/Arrangement.js:
// 20180808 openui5/src/sap.m/test/sap/m/demokit/cart // https://github.com/SAP/openui5/blob/master/src/sap.m/test/sap/m/demokit/cart/webapp/test/integration/arrangement/component/Arrangement.js sap.ui.define([ "sap/ui/test/Opa5", "sap/ui/core/routing/HashChanger", "com/acme/top/wdsp/mon/test/launchers/ushellLauncher" ], function(Opa5, HashChanger, launcher) { "use strict"; // Copy from /sap/ui/test/Opa5-dbg.js function createWaitForObjectWithoutDefaults() { return { // make sure no controls are searched by the defaults viewName: null, controlType: null, id: null, searchOpenDialogs: false, autoWait: false }; } var Arrangement = Opa5.extend("com.acme.top.wdsp.mon.test.integration.arrangement.component.Arrangement", { iStartMyUIComponentInUshell: function(oOptions) { var bComponentLoaded = false; oOptions = oOptions || {}; var oFirstWaitForOptions = createWaitForObjectWithoutDefaults(); oFirstWaitForOptions.success = function() { // include stylesheet var sComponentStyleLocation = jQuery.sap.getModulePath("sap.ui.test.OpaCss", ".css"); $.sap.includeStyleSheet(sComponentStyleLocation); if (oOptions.hash) { HashChanger.getInstance().setHash(oOptions.hash); } launcher.start().then(function() { bComponentLoaded = true; }); }; // wait for starting of component launcher this.waitFor(oFirstWaitForOptions); var oPropertiesForWaitFor = createWaitForObjectWithoutDefaults(); oPropertiesForWaitFor.errorMessage = "Unable to start launcher with hash: " + oOptions.hash; oPropertiesForWaitFor.check = function() { return bComponentLoaded; }; // add timeout to object for waitFor when timeout is specified if (oOptions.timeout) { oPropertiesForWaitFor.timeout = oOptions.timeout; } return this.waitFor(oPropertiesForWaitFor); }, iStartTheApp: function(oOptions) { oOptions = oOptions || {}; return this.iStartMyUIComponentInUshell({ hash: oOptions.hash }); } }); return Arrangement; });
- webapp/test/launchers/ushellLauncher.js:
// Based on https://sapui5.hana.ondemand.com/1.44.38/resources/sap/ui/test/launchers/componentLauncher-dbg.js /*! * UI development toolkit for HTML5 (OpenUI5) * (c) Copyright 2009-2016 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ 'jquery.sap.global' ], function(jQuery) { "use strict"; var $ = jQuery, _loadingStarted = false, _oComponentContainer = null, _$Component = null; /** * By using start launcher will instantiate and place the sap.ushell.Container into html. * By using teardown launcher will destroy the sap.ushell.Container and remove the div from html. * Calling start twice without teardown is not allowed * @private * @class * @author SAP SE * @alias sap.ui.test.launchers.ushellLauncher */ return { start: function() { if (_loadingStarted) { throw "sap.ui.test.launchers.componentLauncher: Start was called twice without teardown"; } var oPromise = Promise.resolve(); _loadingStarted = true; return oPromise.then(function() { var sId = jQuery.sap.uid(); // create and add div to html _$Component = $('<div id="' + sId + '" class="sapUiOpaComponent"></div>'); /* eslint-disable sap-no-dom-insertion */ $("body").append(_$Component).addClass("sapUiOpaBodyComponent"); /* eslint-disable sap-no-dom-insertion */ // create and place the component into html _oComponentContainer = sap.ushell.Container.createRenderer(); _oComponentContainer.placeAt(sId); }); }, hasLaunched: function() { return _loadingStarted; }, teardown: function() { // Opa prevent the case if teardown was called after the start but before the promise was fulfilled if (!_loadingStarted) { throw "sap.ui.test.launchers.componentLauncher: Teardown has been called but there was no start"; } _oComponentContainer.destroy(); _$Component.remove(); _loadingStarted = false; $("body").removeClass("sapUiOpaBodyComponent"); } }; }, /* export= */ true);
Run the test like this:
- webapp/test.html:
<!DOCTYPE html> <html> <head> <title>Testing Overview</title> <!-- try to load the basic UI5 styles --> <link rel="stylesheet" type="text/css" href="resources/sap/ui/core/themes/sap_bluecrystal/library.css"> </head> <body class="sapUiBody sapUiMediumMargin sapUiForceWidthAuto"> <h1>Testing Overview</h1> <p>This is an overview page of various ways to test the generated app during development.<br/>Choose one of the access points below to launch the app as a standalone application, e.g. on a Tomcat server.</p> <ul> <li><a href="test/integration/opaTestsWithComponent.qunit.html?coverage">test/integration/opaTestsWithComponent.qunit.html</a> - run all integration tests with Component</li> </ul> </body> </html>
- webapp/test/integration/opaTestsWithComponent.qunit.html:
<!DOCTYPE html> <html style="overflow:auto"> <head> <title>Integration tests for com.acme.top.wdsp.mon with Component</title> <meta http-equiv='X-UA-Compatible' content='IE=edge'> <meta charset="utf-8"> <script type="text/javascript"> window["sap-ushell-config"] = { defaultRenderer : "fiori2", renderers: { fiori2: { componentData: { config: { rootIntent: "ZF67_WDAYSP-monitor", search: "hidden" } } } }, applications: { "ZF67_WDAYSP-monitor": { "additionalInformation": "SAPUI5.Component=com.acme.top.wdsp.mon", "applicationType": "URL", "url": "../../" } } }; </script> <script src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js" id="sap-ushell-bootstrap"></script> <!-- src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap-ui-core.js" src="../../resources/sap-ui-core.js" --> <!-- The namespace sap.ui.demo.cart.test.arrangement.Arrangement is used to run the IFrame or the component without changes to the test code --> <!--script> // Note 20180809: loading sap-ui-core this way causes all sorts of problems with sap.ushell.Container loading, // regardless of the "preload" setting. It doesn't work, must use the data-sap-ui... solution below. window["sap-ui-config"] = { "animation":"false", "compatVersion":"edge", "debug":"false", "frameOptions":"deny", "language":"en", "theme":"sap_belize", "libs":"sap.m, sap.ushell", "resourceRoots":{ "com.acme.top.wdsp.mon":"../../", "Arrangement":"./arrangement/component/Arrangement"}, "preload":"async", "xx-debugModuleLoading":"true", "xx-showLoadErrors":"true", "xx-supportedLanguages":["en"]}; </script> <script id="sap-ui-bootstrap" src="../../resources/sap-ui-core.js"> </script--> <script id="sap-ui-bootstrap" src="../../resources/sap-ui-core.js" data-sap-ui-animation="false" data-sap-ui-compatVersion="edge" data-sap-ui-frameOptions='deny' data-sap-ui-language="en" data-sap-ui-libs='sap.m, sap.ushell' data-sap-ui-preload='async' data-sap-ui-resourceroots='{ "com.acme.top.wdsp.mon" : "../../", "Arrangement": "./arrangement/component/Arrangement" }' data-sap-ui-theme="sap_belize" data-sap-ui-xx-supportedLanguages=""> </script> <script src="../../resources/sap/ui/qunit/qunit-css.js"></script> <script src="../../resources/sap/ui/thirdparty/qunit.js"></script> <script src="../../resources/sap/ui/qunit/qunit-junit.js"></script> <script src="../../resources/sap/ui/qunit/qunit-coverage.js" data-sap-ui-cover-only="com/acme/top/wdsp/mon/" data-sap-ui-cover-never="[com/acme/top/wdsp/mon/localService/, com/acme/top/wdsp/mon/test/]"> </script> <script> // https://github.com/SAP/openui5/blob/master/src/sap.m/test/sap/m/demokit/cart/webapp/test/integration/opaTestsWithComponent.qunit.html // we want to be able to load our tests asynchronously - pause QUnit until we loaded everything QUnit.config.autostart = false; sap.ui.getCore().attachInit(function () { "use strict"; sap.ui.require([ "sap/ui/fl/FakeLrepConnector", "com/acme/top/wdsp/mon/localService/mockserver", "com/acme/top/wdsp/mon/test/integration/AllJourneys" ], function (FakeLrepConnector, server) { //Fake LREP FakeLrepConnector.enableFakeConnector("../../fakeLRep.json"); // set up test service for local testing server.init({autoRespondAfter: 50}); // configuration has been applied and the tests in the journeys have been loaded - start QUnit QUnit.start(); }); }); </script> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"></div> </body> </html>
- webapp/fakeLRep.json:
{ "changes": [], "settings": { "isKeyUser": true, "isAtoAvailable": false, "isProductiveSystem": false } }
- webapp/test/integration/AllJourneys.js:
// https://github.com/SAP/openui5/blob/master/src/sap.m/test/sap/m/demokit/cart/webapp/test/integration/AllJourneys.js sap.ui.define([ "sap/ui/test/Opa5", "Arrangement", "./ListJourney", //"./BusyJourney" ], function(Opa5, Arrangement) { "use strict"; Opa5.extendConfig({ arrangements: new Arrangement(), viewNamespace: "com.acme.top.wdsp.mon.view.", autoWait: true }); });
- webapp/test/integration/ListJourney.js:
/* global QUnit */ sap.ui.define([ "sap/ui/test/opaQunit", "./pages/List" ], function (opaTest) { "use strict"; QUnit.module("List Page Journey"); opaTest("Should see the list with all entries", function (Given, When, Then) { // Arrangements Given.iStartTheApp(); //Actions When.onTheListPage.iLookAtTheScreen(); // Assertions Then.onTheListPage.iShouldSeeTheList(). and.theListShouldHaveAllEntries(). and.theHeaderShouldDisplayAllEntries(); Then.onTheListPage.iTeardownMyApp(); }); } );
- webapp/test/integration/pages/List.js:
sap.ui.define([ "sap/ui/test/Opa5", "sap/ui/test/actions/Press", "sap/ui/test/actions/EnterText", "sap/ui/test/matchers/AggregationLengthEquals", "sap/ui/test/matchers/AggregationFilled", "sap/ui/test/matchers/PropertyStrictEquals", "com/acme/top/wdsp/mon/test/integration/pages/Common" ], function(Opa5, Press, EnterText, AggregationLengthEquals, AggregationFilled, PropertyStrictEquals, Common) { "use strict"; var sViewName = "List", sSomethingThatCannotBeFound = "*#-Q@@||", iGroupingBoundary = 100; Opa5.createPageObjects({ onTheListPage : { baseClass : Common, actions : { }, assertions : { iShouldSeeTheList : function () { return this.waitFor({ id : "innerUi5Table", viewName : sViewName, success : function (oList) { Opa5.assert.ok(oList, "Found the list"); }, errorMessage : "Can't see the list." }); }, theListShouldHaveAllEntries : function () { var aAllEntities, iExpectedNumberOfItems; // retrieve all TransferSet to be able to check for the total amount return this.waitFor(this.createAWaitForAnEntitySet({ entitySet : "ZF67_C_HRMON", success : function (aEntityData) { aAllEntities = aEntityData; return this.waitFor({ id : "innerUi5Table", viewName : sViewName, matchers : function (oList) { // If there are less items in the table than the growingThreshold, only check for this number. iExpectedNumberOfItems = Math.min(oList.getGrowingThreshold(), aAllEntities.length); return new AggregationLengthEquals({name : "items", length : iExpectedNumberOfItems}).isMatching(oList); }, success : function (oList) { Opa5.assert.strictEqual(oList.getItems().length, iExpectedNumberOfItems, "The table displays all items"); }, errorMessage : "Table does not display all entries." }); } })); }, theHeaderShouldDisplayAllEntries : function () { return this.waitFor({ id : "innerUi5Table", viewName : sViewName, success : function (oList) { var iExpectedLength = oList.getBinding("items").getLength(); this.waitFor({ id : "smartTable", viewName : sViewName, success : function (oSmartTable) { var sTableHeader = oSmartTable.getDomRef().querySelector("div > div > div > span").textContent; Opa5.assert.strictEqual(sTableHeader, "Transfers (11)", "The header should show 'Transfers (11)'"); }, errorMessage : "The table 'smartTable' was not found" }); }, errorMessage : "Table title does not display the number of items in the list" }); }, } } }); } );
- webapp/test/integration/pages/Common.js:
sap.ui.define([ "sap/ui/test/Opa5", "com/acme/top/wdsp/mon/test/launchers/ushellLauncher" ], function(Opa5, launcher) { "use strict"; // Copy from /sap/ui/test/Opa5-dbg.js function createWaitForObjectWithoutDefaults() { return { // make sure no controls are searched by the defaults viewName: null, controlType: null, id: null, searchOpenDialogs: false, autoWait: false }; } return Opa5.extend("com.acme.top.wdsp.mon.test.integration.pages.Common", { iLookAtTheScreen: function() { return this; }, createAWaitForAnEntitySet: function(oOptions) { return { success: function() { var bMockServerAvailable = false, aEntitySet; this.getMockServer().then(function(oMockServer) { aEntitySet = oMockServer.getEntitySetData(oOptions.entitySet); bMockServerAvailable = true; }); return this.waitFor({ check: function() { return bMockServerAvailable; }, success: function() { oOptions.success.call(this, aEntitySet); } }); } }; }, getMockServer: function() { return new Promise(function(success) { (Opa5.getWindow() || window).sap.ui.require(["com/acme/top/wdsp/mon/localService/mockserver"], function(mockserver) { success(mockserver.getMockServer()); }); }); }, iTeardownMyApp: function() { var oOptions = createWaitForObjectWithoutDefaults(); oOptions.success = function() { if (launcher.hasLaunched()) { this.iTeardownMyUIComponentInUshell(); } else { Opa5.prototype.iTeardownMyApp.apply(this, arguments); } }.bind(this); return this.waitFor(oOptions); }, iTeardownMyUIComponentInUshell: function() { var oOptions = createWaitForObjectWithoutDefaults(); oOptions.success = function() { launcher.teardown(); }; return this.waitFor(oOptions); } }); });
- The test runs and code coverage results are now shown, the app runs in the Unified Shell (ushell):
Issue 2: Karma context for OPA5 integration tests
Karma can be used to automate OPA5 tests. As long as tests are not run in an iframe, code coverage reporting works as expected. But karma-openui5 <= 0.2.3 can unfortunately not be used to set up the test context of a launchpad app, because of a problem with loading the ‘sap.ushell’ library when the dependency is given as window[“sap-ui-config”].libs = “sap.m, sap.ushell” (instead of data-sap-ui-libs=’sap.m, sap.ushell’ – see comments in code for ‘opaTestsWithComponent.qunit.html’ above).
How to the up the Karma test context for OPA5 integration tests for launchpad apps?
- package.json:
{ "name": "com.acme.top.wdsp.mon", "version": "1.0.0", "description": "Monitor Tool", "main": "webapp/Component.js", "license": "UNLICENSED", "scripts": { "karma": "karma start karma.conf.js", "test": "npm run karma:ci", "karma:ci": "karma start karma.conf.js --singleRun" }, "author": "Laszlo Kajan", "devDependencies": { "karma": "^2.0.5", "karma-chrome-launcher": "^2.2.0", "karma-cli": "^1.0.1", "karma-coverage": "^1.1.2", "karma-junit-reporter": "^1.2.0", "karma-phantomjs-launcher": "^1.0.4", "karma-qunit": "^2.1.0", "qunit": "^2.6.1" } }
- karma.conf.js:
// Example: Headless OPA5 testing with Karma and PhantomJS // https://blogs.sap.com/2016/11/21/headless-opa5-testing-with-karma-and-phantomjs/ module.exports = function(config) { 'use strict'; const CI_MODE = !!config.singleRun; config.set({ // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: [ 'qunit' ], // list of files / patterns to load in the browser files: [ {pattern: 'node_modules/mobx/lib/mobx.umd.min.js', included: false, served: true, watched: false}, {pattern: 'com.acme.top.eihb/webapp/**/*', included: false, served: true, watched: true}, {pattern: 'sap.ui.mobx/src/**/*', included: false, served: true, watched: true}, {pattern: 'webapp/**/*', included: false, served: true, watched: true}, ], // list of files / patterns to exclude exclude: [ ], customContextFile: "webapp/test/karma/context.html", client: { captureConsole: true, // If false, Karma does not clear the context window upon the completion of running the tests clearContext: false, useIframe: false, qunit: { // showUI: true needs the clearContext: false option to display correctly in non-debug mode //showUI: true, testTimeout: 15000, autostart: false, autoload: false } }, proxies: { '/sap/bc/bsp/sap/zx6g_libmobx/4.1.1/': '/base/node_modules/mobx/lib/', '/sap/bc/ui5_ui5/sap/zf67_eihb_lib/': '/base/com.acme.top.eihb/webapp/', '/sap/bc/ui5_ui5/sap/zx6g_libmobxui5/': '/base/sap.ui.mobx/src/' }, // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: CI_MODE ? { 'webapp/*.js': ['coverage'], 'webapp/!(localService|test)/**/*.js': ['coverage'] } : {}, // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter reporters: ['progress', 'junit'].concat(CI_MODE ? 'coverage' : []), coverageReporter: { dir: 'reports/coverage', // Include all sources files, as indicated by the coverage preprocessor includeAllSources: true, subdir: browser => browser.split(' ')[0], reporters: [ // {type: 'cobertura', subdir: 'cobertura'}, {type: 'lcov', subdir: 'lcov'}, // {type: 'lcovonly', subdir: 'lcovonly'}, {type: 'text-summary'} ] }, junitReporter: { outputDir: 'reports', // results will be saved as $outputDir/$browserName.xml outputFile: 'junit/webapp.xml', // if included, results will be saved as $outputDir/$browserName/$outputFile suite: 'sapui5', // suite will become the package name attribute in xml testsuite element useBrowserName: true // add browser name to report and classes names }, // enable / disable colors in the output (reporters and logs) colors: true, // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG logLevel: config.LOG_ERROR, //loggers: [ // {type: 'console'}, // { // type: 'file', // filename: 'karma.log', // maxLogSize: 65536, // backups: 3 // } //], browserNoActivityTimeout: 30000, // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: [ // 'Chrome' 'Chrome_without_security' // 'PhantomJS_custom' ], customLaunchers: { Chrome_without_security: { base: 'Chrome', flags: ['--disable-web-security'] }, PhantomJS_custom: { base: 'PhantomJS', options: { viewportSize: { width: 1920, height: 1080 }, customHeaders: { DNT: "1" }, windowName: 'my-window', settings: { webSecurityEnabled: false, userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.87 Safari/537.36" } }, flags: ['--load-images=true', '--debug=false', '--disk-cache=false'], debug: false } }, // Have phantomjs exit if a ResourceError is encountered (useful if karma exits without killing phantom) phantomjsLauncher: { exitOnResourceError: false }, // enable / disable watching file and executing tests whenever any file changes // Use `karma run' to run tests if set to false autoWatch: true, // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: true, // Concurrency level // how many browser should be started simultaneous concurrency: Infinity }) } // vim:et:ts=2:
- webapp/test/karma/context.html:
<!DOCTYPE html> <!-- Copy of node_modules/karma/static/context.html This is the execution context. Reloaded before every execution run. --> <html> <head> <title>Integration tests for com.acme.top.wdsp.mon with Component</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> <meta http-equiv='X-UA-Compatible' content='IE=edge'> <meta charset="utf-8"> </head> <body> <!-- The scripts need to be in the body DOM element, as some test running frameworks need the body to have already been created so they can insert their magic into it. For example, if loaded before body, Angular Scenario test framework fails to find the body and crashes and burns in an epic manner. --> <script src="context.js"></script> <script type="text/javascript"> // Configure our Karma and set up bindings %CLIENT_CONFIG% window.__karma__.setupContext(window); // All served files with the latest timestamps %MAPPINGS% </script> <!-- copy from opaTestsWithComponent.qunit.html { --> <script type="text/javascript"> window["sap-ushell-config"] = { defaultRenderer : "fiori2", renderers: { fiori2: { componentData: { config: { rootIntent: "ZF67_WDAYSP-monitor", search: "hidden" } } } }, applications: { "ZF67_WDAYSP-monitor": { "additionalInformation": "SAPUI5.Component=com.acme.top.wdsp.mon", "applicationType": "URL", "url": "/base/webapp/" } } }; </script> <script src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js" id="sap-ushell-bootstrap"></script> <script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap-ui-core.js" data-sap-ui-animation="false" data-sap-ui-compatVersion="edge" data-sap-ui-frameOptions='deny' data-sap-ui-language="en" data-sap-ui-libs='sap.m, sap.ushell' data-sap-ui-preload='async' data-sap-ui-resourceroots='{ "com.acme.top.wdsp.mon" : "/base/webapp/", "Arrangement": "/base/webapp/test/integration/arrangement/component/Arrangement" }' data-sap-ui-theme="sap_belize" data-sap-ui-xx-supportedLanguages=""> </script> <!-- copy from opaTestsWithComponent.qunit.html } --> <!-- Dynamically replaced with <script> tags --> %SCRIPTS% <!-- copy from opaTestsWithComponent.qunit.html { --> <script src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap/ui/qunit/qunit-css.js"></script> <script src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap/ui/thirdparty/qunit.js"></script> <script src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap/ui/qunit/qunit-junit.js"></script> <script src="https://sapui5.hana.ondemand.com/1.44.38/resources/sap/ui/qunit/qunit-coverage.js" data-sap-ui-cover-only="com/acme/top/wdsp/mon/" data-sap-ui-cover-never="[com/acme/top/wdsp/mon/localService/, com/acme/top/wdsp/mon/test/]"> // TODO: data-sap-ui-cover-only="com/acme/top/wdsp/mon/" may be too narrow: include libraries as well? </script> <script> // https://github.com/SAP/openui5/blob/master/src/sap.m/test/sap/m/demokit/cart/webapp/test/integration/opaTestsWithComponent.qunit.html // we want to be able to load our tests asynchronously - pause QUnit until we loaded everything QUnit.config.autostart = false; sap.ui.getCore().attachInit(function () { "use strict"; sap.ui.require([ "sap/ui/fl/FakeLrepConnector", "com/acme/top/wdsp/mon/localService/mockserver", "com/acme/top/wdsp/mon/test/integration/AllJourneys" ], function (FakeLrepConnector, server) { //Fake LREP FakeLrepConnector.enableFakeConnector("/base/webapp/fakeLRep.json"); // set up test service for local testing server.init({autoRespondAfter: 50}); // configuration has been applied and the tests in the journeys have been loaded - start QUnit QUnit.start(); }); }); </script> <!-- copy from opaTestsWithComponent.qunit.html } --> <script type="text/javascript"> window.__karma__.loaded(); </script> </body> </html>
- Run the tests like this:
#!/bin/sh -e export PATH=./node_modules/.bin:$PATH; export CHROME_BIN=chromium; npm install; Xvfb :99 & export XVFB_PID=$!; export DISPLAY=:99.0; npm test;
- Read the reports:
- junit: reports/*/junit/webapp.xml
- coverage: reports/coverage/lcov/lcov-report/index.html
Summary
This blog post showed you how to run OPA5 integration tests for your Fiori launchpad app.
You are now also able to automate the running of the tests using Karma.
Further reading
- Headless OPA5 testing with Karma and PhantomJS
- Karma
- karma-coverage
- karma-openui5
- karma-openui5-sample
- karma-openui5-maven-sample
- karma-qunit
- SAP openui5 demo cart – Update tests to new best practices
- SAPUI5 – class sap.ui.test.Opa5
- SAPUI5 – Code Coverage
- stack overflow – Code coverage in sapui5 OPA5 tests
Afterword
Thank you for reading through this blog post. I hope you found it useful. I hope it raised questions as well. Do follow the above, and other links to satisfy your curiosity – a sure way to deepen your understanding.
Please check out my other blog posts for interesting topics, like “Adding Reactive State Management with Validation to Existing UI5 Application – a Tutorial“.
Just tried it for another app, and worked perfectly.
Hi Laszlo,
terrific article.
we are trying to adapt your example to use with GitLab CICD using Grunt to run the tests.
we seem to be experiencing major issues - the tests seem to run but they all seem to time out waiting for the mock data to be loaded to the various controls in our test app.
Would it be possible to get a copy of your app code so that we can explore it more better.
thank you in advance.
cheers.
Pas.
Hello Pas!
The mock server arrangement is the standard one.
Make sure your mock server is started, before you run the tests – check out webapp/test/karma/context.html above.
I will make a sample app available when I find the time.
Best regards,
Laszlo
Hi Laszlo,
Thanks for the reply.
have studied your code a bit more, have reviewed our code and it turns out that we were not setting the binding context to 'complex' in the boot strap.
once again, thanks for your reply.
Cheers
Pas.