Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
vobu
Active Contributor

Testing UI5 apps



As you can see from the TOC above, this will be a multi-part blog post series covering the aspect of testing UI5 apps - a topic every UI5 developer is interested in but few actually utilise.

Let's change that 🙂

(Note: I'll link the above parts once they're published)

Setup


There are two ways of running tests:

1) manually in a browser

2) automatically via some Continuous Integration scenario

We'll look at 1) in the first three parts of the series and touch 2) from part three on.

Run it locally alongside


In order to get a clear grasp on things, I recommend running the demo application and its various test suites on your local machine now. So it gets easy to follow up "in code" what's written in the blog posts.

The code along with installation instructions is located at https://github.com/vobujs/openui5-sample-app-testing - yep, by the URL you can already tell that I copied the official sample ToDo UI5 app and modified it to fit the purpose of this blog series about testing.

So, clone the git repo and given that you have nodejs installed, essentially do
npm install --global grunt-cli
cd $your_git_clone_dir
npm install
grunt serve
(open browser and point to
http://localhost:8080/test/unit/unitTests.qunit.html)

After installation, the test-relevant directory layout of the sample app is this:
webapp
<snip />
├── test
│ ├── integration
│ │ ├── AllJourneys.js
│ │ ├── FilterJourney.js
│ │ ├── SearchJourney.js
│ │ ├── TodoListJourney.js
│ │ ├── opaTests.qunit.html
│ │ └── pages
│ │ ├── App.js
│ │ └── Common.js
│ ├── testsuite.qunit.html
│ └── unit
│ ├── allTests.js
│ ├── controller
│ │ └── App.controller.js
│ └── unitTests.qunit.html
<snip />

Bootstrapping manual Unit Tests


Running Unit Tests manually means bootstrapping your UI5 application in a QUnit-environment. Luckily there's ready-to-use code for that in more places than you'd think: e.g. the UI5 documentation, in the WebIDE application templates and in the official sample ToDo UI5 app.

We'll take the latter as a reference, slightly modified to use the UI5 CDN instead of local path references and QUnit 2 instead of QUnit 1 (see below for more info on that): (file on github)
<!DOCTYPE html>
<html>
<head>
<title>Unit tests for Todo List</title>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta charset="utf-8">

<script id="sap-ui-bootstrap"
src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-theme="sap_belize"
data-sap-ui-bindingSyntax="complex"
data-sap-ui-compatVersion="edge"
data-sap-ui-preload="async"
data-sap-ui-resourceRoots='{"sap.ui.demo.todo": "../../"}'>
</script>

<!--<script src=".https://sapui5.hana.ondemand.com/resources/sap/ui/thirdparty/qunit.js"></script>-->
<!--<script src="https://sapui5.hana.ondemand.com/resources/sap/ui/qunit/qunit-css.js"></script>-->

<!-- use QUnit v2 instead of v1 - v1 is above -->
<script src="https://sapui5.hana.ondemand.com/resources/sap/ui/thirdparty/qunit-2.js"></script>
<script src="https://sapui5.hana.ondemand.com/resources/sap/ui/qunit/qunit-2-css.js"></script>

<script>
QUnit.config.autostart = false;
sap.ui.getCore().attachInit(function() {
sap.ui.require([
"sap/ui/demo/todo/test/unit/allTests"
], function() {
QUnit.start();
});
});
</script>

</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
</body>
</html>

It's an html file just as index.html (for starting your standalone UI5 app), but by convention named unitTests.qunit.html and located at $your_project/webapp/test/unit/.

You'll recognise most of the parts - there's the bootstrap script tag <script id="sap-ui-bootstrap" ...></script> along with starting the application after UI5's core has "booted" up (sap.ui.getCore().attachInit(...)). (You did know about the strict asynchronous coding approach you should follow with UI5 so you're prepared for the UI5 evolution to come, right? Right?!?).

QUnit 1 vs. 2


The QUnit-specific part is including the relevant libraries (<script src=".../qunit-2..."></script>) along with starting QUnit itself (QUnit.start()). Note that QUnit 2 is referenced instead of QUnit 1. Why? We'll get to that in part three of the series, but as a brief glimpse ahead: version 2 has some essential capabilities such as

  • being able to run a single test via QUnit.only() instead of always executing the entire module

  • use a setup and teardown section before/after all Tests (via before and after) instead of having this for each test only (beforeEach, afterEach)


QUnit 2 was introduced to the UI5 core with version 1.48 (thanks, Michadelic!) and exists alongside QUnit 1 - so no need to port anything (yet) if you can pass on the QUnit 2 features.

On the other hand, this means being attentive to the UI5 version your project is using: if <1.48, no QUnit 2-features for you.

Application under Test, Mama Testfile


The reference to the application whose parts should be tested is done primarily via providing the appropriate resoureRoot in the bootstrap: here data-sap-ui-resourceRoots='{"sap.ui.demo.todo": "../../"} tells the QUnit-UI5-environment to look for the application files in $your_project/webapp (given that the unitTests.qunit.html is located at $your_project/webapp/test/unit/).

Any parts of the application (controllers, formatters, controls, ...) are then referenced in the tests themselves. We'll come to that later. But in turn, all the actual Unit Tests are "collected" in the single file $your_project/webapp/test/unit/allTests.js that is required() in the above attachInit callback.

So this is a typical directory structure for your Unit Tests:
webapp/test/unit/
├── allTests.js
├── controller
│ └── App.controller.js
└── unitTests.qunit.html

Coming back to unitTests.qunit.html, the two <div>s in body fit a special purpose:

qunit is where the HTML output of the Unit Tests will be injected,
qunit-fixture is intended as the container for DOM-relevant runtime operations. More on that later in part four of the blog series.

Unit Testing


By now, the setup, file locations and purposes should be clear - let's move on to the actual testing part, beginning with Unit Tests.

In UI5-verse, Unit Tests are intended for functional testing. That is, verifying any functionality "under the hood" of your application that doesn't necessarily need to have a UI element/interaction associated.

So it's more about making sure what your application does than how it's triggered. Think of it with the beloved car metaphor in programming: Unit Tests check the way the engine works: gearbox and clutch combinations, throttle cable clearance, gearshift wheel intertwine, ... you get the idea. To verify that, the engine doesn't have to be in a chassis - it will work outside one just fine. Testing how chassis and engine work together would then be a task for an integration test - part two of this blog series.

test structure


In the above file system layout snippet, $your_project/webapp/test/unit/controller/App.controller.js contains the Unit Tests pertaining to $your_project/webapp/controller/App.controller.js - the file name resemblance is by convention only.

In essence, a Unit Test file looks like this:
sap.ui.define([
"sap/ui/demo/todo/controller/App.controller"
], function(AppController) {
"use strict";

QUnit.module("test group");

QUnit.test("verify value and type", function(assert) {
// implementation
var vSomeVar = "42";
assert.strictEqual(42, vSomeVar, "42 and " + vSomeVar + " have the same value and type");
// assert will fail
});

// pidgin code below
QUnit.module("...");

QUnit.test("...");
QUnit.test("...");
QUnit.test("...");

QUnit.module("...");

QUnit.test("...");
QUnit.test("...");
//...
});

The sap.ui.define syntax is the same as in application coding, providing an asynchronous module loading mechanism. A controller file is loaded and referenced at runtime via AppController.

QUnit.module "groups" all subsequent tests under the given name.

QUnit.test declares the actual Unit Test - with the variable assert being at the center of it. assert is intended for the final check of a condition that has previously been induced.

assert has a descriptive API for testing conditions.
The above assert.strictEqual will fail: the integer 42 and the string "42" are not of the same type.
On the other hand, the loosely typed check via

assert.equal(42, vSomeVar, "42 and " + vSomeVar + " have the same value");


would work, checking the value only.

The API docs provide plenty of assert-examples. How to reference the UI5 application coding for testing is illustrated in the next section.

Some common UI5 Unit Test cases


...with no claim to be complete or representative 😉

Testing a standalone controller method


If there are methods in the controller that have little to no dependencies to other UI5 modules, then testing these is as simple as instantiating the controller and calling it's methods.

This even holds true for the init method onInit():
sap.ui.define([
"sap/ui/demo/todo/controller/App.controller"
], function(AppController) {
"use strict";

QUnit.test("standalone controller method w/o dependencies", function(assert) {
// arrangement
var oController = new AppController();

// action
oController.onInit();

// assertions
assert.ok(oController.aSearchFilters);
assert.ok(oController.aTabFilters);
});
});

Testing a controller method with dependencies to other UI5 modules and/or functions


Even though youShouldStickToMVC™, it is not uncommon that controller logic accesses the view directly instead of interacting with the model only. Even to the point where the DOM is accessed and further processed e.g. with jQuery.
// pidgin controller code
method: function() {
var oControl = this.getView().byId("someControlId");
var $control = oControl.getDomRef();
jQuery($control).animate(...);
}

This creates a dependency of the controller code to other UI5 runtime artefacts, such as sap.ui.core.mvc.Controller.getView() or sap.ui.core.Element.getDomRef().

But the QUnit test environment doesn't have access to the full control structure and DOM the UI5 spawns at runtime (in contrast to the OPA tests, covered in part 2 of the series). In oder to mitigate that, both the view and parts of the DOM need to be "stubbed" - aka simulated, mocked, decoupled.

In the UI5 framework, sinon is included for that purpose. Via sinon.stub(), methods can be "overwritten" with specific code instructions:
// pidgin code
sinon.stub(Module, methodName).returns(customCode);

With this approach, views, models and even DOM elements can be stubbed:
sap.ui.define([
"sap/ui/base/ManagedObject",
"sap/ui/core/mvc/Controller",
"sap/ui/demo/todo/controller/App.controller",
"sap/ui/model/json/JSONModel",
"sap/ui/thirdparty/sinon",
"sap/ui/thirdparty/sinon-qunit"
], function(ManagedObject, Controller, AppController, JSONModel/*, sinon, sinonQunit*/) {
"use strict";

QUnit.test("controller method that uses getView and getDomRef", function (assert) {
//// begin arrangements
// regular init of controller
var oController = new AppController();
// regular init of a JSON model
var oJsonModelStub = new JSONModel({});
// construct a dummy DOM element
var oDomElementStub = document.createElement("div");
// construct a dummy View
var oViewStub = new ManagedObject({});

// mock View.byId().getDomRef()
oViewStub.byId = function(sNeverUsed) {
return {
getDomRef : function() {
return oDomElementStub;
}
}
};

// regular setting of a model to a View
oViewStub.setModel(oJsonModelStub);

// stubbing Controller.getView() to return our dummy view object
var oGetViewStub = sinon.stub(Controller.prototype, "getView").returns(oViewStub);
//// end arrangements

// prepare data model for controller method
oJsonModelStub.setProperty("/newTodo", "some new item");

// actual test call!
oController.addTodo();

// check result of test call
assert.strictEqual(oJsonModelStub.getProperty("/todos").length, 1, "1 new todo item was added");

// follow-up: never forget to un-stub aka
// restore the original behavior, here: Controller.prototype.getView()
oGetViewStub.restore();
});
});

 

Testing asynchronous code execution


Ahhh, executing asynchronous code at runtime, my favourite 🙂

A typical application of this is via Promises, implemented for example in the UI5 framework for sap.ui.model.odata.v2.ODataModel.metadataLoaded(). (And don't worry about cross-browser compatibility, UI5 carries a shim for problematic candidates such as IE11, so you can safely use the global Promise object).

For demonstration purposes, here's retrieving the ToDos from our sample app Promise-style:
/**
* demonstration purpose only: retrieve todos from JSON model
* async-Promise-style
*
* @return {Promise}
*/
getTodosViaPromise: function () {
return new Promise(function (fnResolve, fnReject) {
var oModel = this.getView().getModel();
if (!oModel) {
fnReject("couldn't load the application model")
} else {
fnResolve(oModel.getProperty("/todos"));
}
}.bind(this))
},

 

The corresponding Unit Test uses assert.async() to let the QUnit framework know to expect an asynchronous test.

After the (final) assert, the async helper function is called to signal QUnit the test is finished.
sap.ui.define([
"sap/ui/base/ManagedObject",
"sap/ui/core/mvc/Controller",
"sap/ui/demo/todo/controller/App.controller",
"sap/ui/model/json/JSONModel",
"sap/ui/thirdparty/sinon",
"sap/ui/thirdparty/sinon-qunit"
], function (ManagedObject, Controller, AppController, JSONModel/*, sinon, sinonQunit*/) {
"use strict";

QUnit.test("async function in controller", function (assert) {
// tell QUnit to wait for it
var fnDone = assert.async();

// arrangements
// regular init of controller
var oController = new AppController();
// regular init of a JSON model
var oJsonModelStub = new JSONModel({
"todos": []
});
// construct a dummy View
var oViewStub = new ManagedObject({});
// regular setting of a model to a View
oViewStub.setModel(oJsonModelStub);
// stubbing Controller.getView() to return our dummy view object
var oGetViewStub = sinon.stub(Controller.prototype, "getView").returns(oViewStub);

// action + assertion: start the Promise chain!
oController.getTodosViaPromise()
.then(function (aTodos) {
assert.ok(aTodos.length >= 0, "todos exist (zero or more)");
})
.then(oGetViewStub.restore) // follow-up: never forget to un-stub!
.then(fnDone) // tell QUnit test is finished
// never forget to catch potential errors in the promise chain
// and do proper clean up
.catch(function (oError) {
assert.ok(false, "Error occured: " + oError);
// follow-up: never forget to un-stub!
oGetViewStub.restore();
// tell QUnit test is finished
fnDone();
});
});
});

Conclusion


This concludes the first part of the blog series about testing UI5 applications.

A custom html-file unitTests.qunit.html was used for bootstrapping QUnit and the application under test for executing Unit Tests.

In addition to showing the structure of both the file system layout and the test files themselves, some common Unit Test cases were explained that should apply to most of the UI5 development out there.

Next up: running integration (OPA) tests for your UI5 application!
5 Comments
Labels in this area