Technical Articles
Testing UI5 apps, part 1: setup and Unit Testing
Testing UI5 apps
- Part1: Setup and Unit Testing (this article)
- Part2: Integration aka OPA Testing
- Part3.1: Mockserver
- Part3.2: Code Coverage and other necessary Usefulities
- Part4: Advanced Testing mumbo-jambo
- Part5: Numbers, Experiences and Business Impact
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
andafter
) 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!
You are welcome Volker - enjoy QUnit V2 🙂
Great blog, looking forward to the next Ines.
Michael
Promising blog Volker Buzek ,
I am wondering why you are mocking oViewStub.byId in that way. Isn't it better to use sinon to mock everything?
Thanks,
Marcos.
Hi,
no particular reason why I didn't use sinon for the multi-level stub. Your solution looks better by the books though.
Just validated it, but still needs
prior to the sinon stub to work properly.
Thanks for the hint,
V.
Volker Buzek , do you happen to have any example code for using mock data in a unit test? For instance, I would like to be able to unit test a function that sets the visibility of a particular button based on a value from my mock data (and ultimately from my odata service) - how would I go about that?
Thanks,
Michael
hi, there are a couple of options:
sap.ui.define(["./path/to/json-data", ...], (jsonData) => ({ ... new JSONModel(jsonData) })
fetch
: (attn, pseudo-code)fetch(uri).then(data => data.json).then(json => JSONModel.setData(json)).catch(err => console.error(err)
beforeEach() { }
 to setup static mock data for each test in a suitehth, happy testing!
-v