Skip to Content

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!

To report this post you need to login first.

3 Comments

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

  1. Marcos Canales

    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?

    // Mock View.byId().getDomRef()
    var oViewByIdStub = sinon.stub(oViewStub, 'byId').returns({
        getDomRef: function() {
            return oDomElementStub;
        }
    });
    
    // Moreover, the original behavior can be restored in case it's needed for any reason
    //oViewByIdStub.restore();

    Thanks,

    Marcos.

    (0) 
    1. Volker Buzek Post author

      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

      oViewStub.byId = function (sNeverUsed) {};

      prior to the sinon stub to work properly.

      Thanks for the hint,

      V.

       

      (0) 

Leave a Reply